dzql 0.1.0-alpha.1 → 0.1.0-alpha.2

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,559 @@
1
+ # Getting Started with DZQL
2
+
3
+ DZQL is a PostgreSQL-powered framework that provides automatic CRUD operations via WebSocket RPC with zero boilerplate. This guide walks you through setting up your first DZQL project.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Bun** 1.0+ (Node.js not required!)
8
+ - **Docker** and **Docker Compose** (for PostgreSQL)
9
+ - A code editor
10
+
11
+ ## Quick Start (5 minutes)
12
+
13
+ ### 1. Create a New Project
14
+
15
+ ```bash
16
+ mkdir my-dzql-app
17
+ cd my-dzql-app
18
+ bun init
19
+ ```
20
+
21
+ ### 2. Install DZQL
22
+
23
+ ```bash
24
+ bun add dzql
25
+ ```
26
+
27
+ ### 3. Set Up PostgreSQL with Docker
28
+
29
+ Create a `docker-compose.yml`:
30
+
31
+ **For standalone projects (using npm/bun):**
32
+ ```yaml
33
+ services:
34
+ postgres:
35
+ image: postgres:latest
36
+ environment:
37
+ POSTGRES_USER: dzql
38
+ POSTGRES_PASSWORD: dzql
39
+ POSTGRES_DB: dzql
40
+ volumes:
41
+ # DZQL Core System migrations
42
+ - node_modules/dzql/src/database/migrations/001_schema.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro
43
+ - node_modules/dzql/src/database/migrations/002_functions.sql:/docker-entrypoint-initdb.d/002_functions.sql:ro
44
+ - node_modules/dzql/src/database/migrations/003_operations.sql:/docker-entrypoint-initdb.d/003_operations.sql:ro
45
+ - node_modules/dzql/src/database/migrations/004_search.sql:/docker-entrypoint-initdb.d/004_search.sql:ro
46
+ - node_modules/dzql/src/database/migrations/005_entities.sql:/docker-entrypoint-initdb.d/005_entities.sql:ro
47
+ - node_modules/dzql/src/database/migrations/006_auth.sql:/docker-entrypoint-initdb.d/006_auth.sql:ro
48
+ - node_modules/dzql/src/database/migrations/007_events.sql:/docker-entrypoint-initdb.d/007_events.sql:ro
49
+ - node_modules/dzql/src/database/migrations/008_hello.sql:/docker-entrypoint-initdb.d/008_hello.sql:ro
50
+ - node_modules/dzql/src/database/migrations/008a_meta.sql:/docker-entrypoint-initdb.d/008a_meta.sql:ro
51
+ # Your domain-specific migrations
52
+ - ./init_db:/docker-entrypoint-initdb.d/init_db:ro
53
+ ports:
54
+ - "5432:5432"
55
+ ```
56
+
57
+ **For monorepo projects (like the venues example):**
58
+ ```yaml
59
+ services:
60
+ postgres:
61
+ image: postgres:latest
62
+ environment:
63
+ POSTGRES_USER: dzql
64
+ POSTGRES_PASSWORD: dzql
65
+ POSTGRES_DB: dzql
66
+ volumes:
67
+ # DZQL Core System migrations (relative path from monorepo root)
68
+ - ../../dzql/src/database/migrations/001_schema.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro
69
+ - ../../dzql/src/database/migrations/002_functions.sql:/docker-entrypoint-initdb.d/002_functions.sql:ro
70
+ - ../../dzql/src/database/migrations/003_operations.sql:/docker-entrypoint-initdb.d/003_operations.sql:ro
71
+ - ../../dzql/src/database/migrations/004_search.sql:/docker-entrypoint-initdb.d/004_search.sql:ro
72
+ - ../../dzql/src/database/migrations/005_entities.sql:/docker-entrypoint-initdb.d/005_entities.sql:ro
73
+ - ../../dzql/src/database/migrations/006_auth.sql:/docker-entrypoint-initdb.d/006_auth.sql:ro
74
+ - ../../dzql/src/database/migrations/007_events.sql:/docker-entrypoint-initdb.d/007_events.sql:ro
75
+ - ../../dzql/src/database/migrations/008_hello.sql:/docker-entrypoint-initdb.d/008_hello.sql:ro
76
+ - ../../dzql/src/database/migrations/008a_meta.sql:/docker-entrypoint-initdb.d/008a_meta.sql:ro
77
+ # Your domain-specific migrations
78
+ - ./init_db:/docker-entrypoint-initdb.d/init_db:ro
79
+ ports:
80
+ - "5432:5432"
81
+ ```
82
+
83
+ Start PostgreSQL:
84
+
85
+ ```bash
86
+ docker compose up -d
87
+ ```
88
+
89
+ ### 4. Initialize Database
90
+
91
+ The docker-compose.yml automatically runs DZQL core migrations from `node_modules/dzql/src/database/migrations/`.
92
+
93
+ Create your domain-specific migrations in `database/init_db/001_domain.sql`:
94
+
95
+ ```sql
96
+ -- Create your tables (after DZQL core is set up)
97
+ CREATE TABLE IF NOT EXISTS users (
98
+ id SERIAL PRIMARY KEY,
99
+ name TEXT NOT NULL,
100
+ email TEXT NOT NULL UNIQUE,
101
+ created_at TIMESTAMPTZ DEFAULT NOW()
102
+ );
103
+
104
+ -- Register your entities with DZQL
105
+ SELECT dzql.register_entity(
106
+ p_table_name := 'users',
107
+ p_label_field := 'name',
108
+ p_searchable_fields := array['name', 'email'],
109
+ p_fk_includes := '{}'::jsonb
110
+ );
111
+ ```
112
+
113
+ **Important:** Your migrations run AFTER the DZQL core migrations, so entity registration functions are available.
114
+
115
+ Start PostgreSQL:
116
+
117
+ ```bash
118
+ docker compose up -d
119
+ ```
120
+
121
+ The Docker Compose file will automatically run all DZQL core migrations first, then your domain migrations.
122
+
123
+ ### 5. Create Your Server
124
+
125
+ Create `server/index.js`:
126
+
127
+ ```javascript
128
+ import { createServer } from 'dzql';
129
+
130
+ const server = createServer({
131
+ port: process.env.PORT || 3000,
132
+ // Optional: custom API functions
133
+ customApi: {},
134
+ // Optional: static file serving
135
+ staticPath: './public'
136
+ });
137
+
138
+ console.log(`🚀 Server running on port ${server.port}`);
139
+ ```
140
+
141
+ ### 6. Start Your Server
142
+
143
+ ```bash
144
+ bun server/index.js
145
+ ```
146
+
147
+ Your DZQL server is now running at `ws://localhost:3000/ws`!
148
+
149
+ ## Project Setup: Standalone vs Monorepo
150
+
151
+ ### Standalone Project (Recommended for Most Users)
152
+ Use `node_modules/dzql/src/database/migrations/` paths in your docker-compose.yml
153
+
154
+ ```
155
+ my-app/
156
+ ├── database/
157
+ │ ├── docker-compose.yml # Uses node_modules paths
158
+ │ └── init_db/
159
+ │ └── 001_domain.sql
160
+ ├── server/
161
+ ├── client/
162
+ └── package.json
163
+ ```
164
+
165
+ ### Monorepo Project (Like the Venues Example)
166
+ Use relative paths `../../dzql/src/database/migrations/` in docker-compose.yml
167
+
168
+ ```
169
+ monorepo/
170
+ ├── packages/
171
+ │ ├── dzql/ # Framework package
172
+ │ │ └── src/database/migrations/ # Core migrations
173
+ │ └── my-app/ # Your app
174
+ │ ├── database/
175
+ │ │ └── docker-compose.yml # References ../../dzql/...
176
+ │ ├── server/
177
+ │ └── package.json
178
+ ```
179
+
180
+ ## Using DZQL in Your Client
181
+
182
+ ### Browser/Frontend
183
+
184
+ ```javascript
185
+ import { useWs, WebSocketManager } from 'dzql/client';
186
+
187
+ // Create a fresh WebSocket connection
188
+ const ws = new WebSocketManager();
189
+
190
+ // Connect to server
191
+ await ws.connect();
192
+
193
+ // Login
194
+ const result = await ws.api.login_user({
195
+ email: 'user@example.com',
196
+ password: 'password123'
197
+ });
198
+
199
+ // Use DZQL operations - all 5 operations work the same way:
200
+ // GET, SAVE, DELETE, LOOKUP, SEARCH
201
+
202
+ // GET - Retrieve a single record
203
+ const user = await ws.api.get.users({ id: 1 });
204
+
205
+ // SAVE - Create or update (upsert)
206
+ const newUser = await ws.api.save.users({
207
+ name: 'John Doe',
208
+ email: 'john@example.com'
209
+ });
210
+
211
+ // LOOKUP - Autocomplete/suggestions
212
+ const suggestions = await ws.api.lookup.users({
213
+ p_filter: 'john'
214
+ });
215
+
216
+ // SEARCH - Full search with filters and pagination
217
+ const results = await ws.api.search.users({
218
+ filters: {
219
+ name: { ilike: '%john%' },
220
+ email: 'john@example.com'
221
+ },
222
+ page: 1,
223
+ limit: 10
224
+ });
225
+
226
+ // DELETE - Remove a record
227
+ const deleted = await ws.api.delete.users({ id: 1 });
228
+
229
+ // When done
230
+ ws.cleanDisconnect();
231
+ ```
232
+
233
+ ### Bun/Backend
234
+
235
+ ```javascript
236
+ import { db, sql } from 'dzql';
237
+
238
+ // Direct database queries
239
+ const users = await sql`SELECT * FROM users`;
240
+
241
+ // DZQL operations (require userId)
242
+ const user = await db.api.get.users({ id: 1 }, userId);
243
+ const saved = await db.api.save.users({ name: 'John' }, userId);
244
+ const searched = await db.api.search.users({ filters: {} }, userId);
245
+ ```
246
+
247
+ ## Project Structure
248
+
249
+ A typical DZQL project looks like:
250
+
251
+ ```
252
+ my-dzql-app/
253
+ ├── server/
254
+ │ ├── index.js # Server entry point
255
+ │ └── api.js # Custom API functions (optional)
256
+ ├── database/
257
+ │ └── init_db/
258
+ │ ├── 001_domain.sql # Your schema & entity registration
259
+ │ └── 002_seed.sql # Sample data (optional)
260
+ ├── public/ # Static files (optional)
261
+ │ └── index.html
262
+ ├── docker-compose.yml
263
+ ├── bunfig.toml # Bun config (optional)
264
+ └── package.json
265
+ ```
266
+
267
+ ## The 5 DZQL Operations
268
+
269
+ Every registered entity automatically gets these 5 operations:
270
+
271
+ ### 1. GET - Single Record
272
+ ```javascript
273
+ const record = await ws.api.get.tableName({ id: 1 });
274
+ // Throws "record not found" error if not exists
275
+ ```
276
+
277
+ ### 2. SAVE - Upsert
278
+ ```javascript
279
+ const record = await ws.api.save.tableName({
280
+ id: 1, // Optional - omit for insert
281
+ name: 'Updated'
282
+ });
283
+ ```
284
+
285
+ ### 3. DELETE - Remove Record
286
+ ```javascript
287
+ const deleted = await ws.api.delete.tableName({ id: 1 });
288
+ ```
289
+
290
+ ### 4. LOOKUP - Autocomplete
291
+ ```javascript
292
+ const options = await ws.api.lookup.tableName({
293
+ p_filter: 'search term'
294
+ });
295
+ // Returns: [{ label: 'Display Name', value: 1 }, ...]
296
+ ```
297
+
298
+ ### 5. SEARCH - Advanced Search
299
+ ```javascript
300
+ const results = await ws.api.search.tableName({
301
+ filters: {
302
+ name: { ilike: '%john%' },
303
+ age: { gte: 18 },
304
+ city: ['NYC', 'LA'],
305
+ active: true
306
+ },
307
+ sort: { field: 'name', order: 'asc' },
308
+ page: 1,
309
+ limit: 25
310
+ });
311
+ // Returns: { data: [...], total: 100, page: 1, limit: 25 }
312
+ ```
313
+
314
+ ## Entity Registration
315
+
316
+ Before DZQL can work with a table, you must register it with the `dzql.register_entity()` function.
317
+
318
+ **Important:** This function is provided by DZQL's core migrations, which run automatically when PostgreSQL starts via docker-compose.
319
+
320
+ ```sql
321
+ SELECT dzql.register_entity(
322
+ p_table_name := 'venues', -- Your table name
323
+ p_label_field := 'name', -- Used by LOOKUP
324
+ p_searchable_fields := array['name', 'address'], -- Used by SEARCH
325
+ p_fk_includes := '{"org": "organisations"}'::jsonb, -- Dereference FKs
326
+ p_graph_rules := '{}'::jsonb -- Optional: automation
327
+ );
328
+ ```
329
+
330
+ **Parameters:**
331
+ - `p_table_name`: Your PostgreSQL table name
332
+ - `p_label_field`: Which field to use for display (LOOKUP)
333
+ - `p_searchable_fields`: Fields searchable by SEARCH
334
+ - `p_fk_includes`: Foreign keys to auto-dereference (optional)
335
+ - `p_graph_rules`: Graph rules for automation (optional)
336
+
337
+ See the [venues example](https://github.com/blueshed/dzql/blob/main/packages/venues/database/init_db/009_venues_domain.sql) for a complete schema with multiple entity registrations.
338
+
339
+ ## Custom API Functions
340
+
341
+ Add custom functions that work alongside DZQL operations:
342
+
343
+ **PostgreSQL Function:**
344
+ ```sql
345
+ CREATE OR REPLACE FUNCTION my_function(
346
+ p_user_id INT,
347
+ p_name TEXT
348
+ ) RETURNS TABLE (message TEXT) AS $$
349
+ BEGIN
350
+ RETURN QUERY SELECT 'Hello, ' || p_name;
351
+ END;
352
+ $$ LANGUAGE plpgsql;
353
+ ```
354
+
355
+ **Call from Client:**
356
+ ```javascript
357
+ const result = await ws.api.my_function({ name: 'World' });
358
+ // Returns: { message: 'Hello, World' }
359
+ ```
360
+
361
+ **Bun Function:**
362
+ ```javascript
363
+ // server/api.js
364
+ export async function my_function(userId, params = {}) {
365
+ return {
366
+ message: `Hello, ${params.name}!`,
367
+ user_id: userId
368
+ };
369
+ }
370
+ ```
371
+
372
+ Then pass it to createServer:
373
+ ```javascript
374
+ const customApi = await import('./api.js');
375
+ const server = createServer({
376
+ customApi
377
+ });
378
+ ```
379
+
380
+ ## Real-time Events
381
+
382
+ DZQL broadcasts changes in real-time via WebSocket:
383
+
384
+ ```javascript
385
+ // Listen for all events
386
+ const unsubscribe = ws.onBroadcast((method, params) => {
387
+ console.log(`Event: ${method}`, params);
388
+ // method: "users:insert", "users:update", "users:delete"
389
+ });
390
+
391
+ // Stop listening
392
+ unsubscribe();
393
+ ```
394
+
395
+ ## Authentication
396
+
397
+ DZQL provides built-in user authentication:
398
+
399
+ ```javascript
400
+ // Register
401
+ const result = await ws.api.register_user({
402
+ email: 'user@example.com',
403
+ password: 'secure-password'
404
+ });
405
+
406
+ // Login
407
+ const result = await ws.api.login_user({
408
+ email: 'user@example.com',
409
+ password: 'secure-password'
410
+ });
411
+ // Returns: { token, profile, user_id }
412
+
413
+ // Logout
414
+ await ws.api.logout();
415
+
416
+ // Save token for auto-login
417
+ localStorage.setItem('dzql_token', result.token);
418
+
419
+ // Auto-connect with token
420
+ const ws = new WebSocketManager();
421
+ await ws.connect(); // Automatically uses token from localStorage
422
+ ```
423
+
424
+ ## Error Handling
425
+
426
+ ```javascript
427
+ try {
428
+ const user = await ws.api.get.users({ id: 999 });
429
+ } catch (error) {
430
+ console.error(error.message);
431
+ // "record not found" - record doesn't exist
432
+ // "Permission denied: view on users" - access denied
433
+ // "Function not found" - custom function doesn't exist
434
+ }
435
+ ```
436
+
437
+ ## Environment Variables
438
+
439
+ ```bash
440
+ # Server
441
+ PORT=3000
442
+ DATABASE_URL=postgresql://dzql:dzql@localhost:5432/dzql
443
+ NODE_ENV=development
444
+ LOG_LEVEL=INFO
445
+
446
+ # JWT
447
+ JWT_SECRET=your-secret-key
448
+ JWT_EXPIRES_IN=7d
449
+
450
+ # WebSocket
451
+ WS_PING_INTERVAL=30000
452
+ WS_PING_TIMEOUT=5000
453
+ ```
454
+
455
+ ## Running Tests
456
+
457
+ ```bash
458
+ bun test tests/
459
+ ```
460
+
461
+ ## Troubleshooting
462
+
463
+ ### Database won't connect
464
+ ```bash
465
+ # Check if PostgreSQL is running
466
+ docker ps
467
+ # Check logs
468
+ docker compose logs postgres
469
+ # Restart
470
+ docker compose down -v && docker compose up -d
471
+ ```
472
+
473
+ ### WebSocket connection fails
474
+ - Ensure server is running: `http://localhost:3000`
475
+ - Check firewall for port 3000
476
+ - Check browser console for errors
477
+
478
+ ### Entity not found errors
479
+ - Verify table is created in database
480
+ - Verify entity is registered with `dzql.register_entity()`
481
+ - Check `p_table_name` matches exactly
482
+
483
+ ### Permission denied errors
484
+ - Implement permission rules in your entity registration
485
+ - Check user authentication status
486
+ - Verify user_id is being passed correctly
487
+
488
+ ## Next Steps
489
+
490
+ 1. **Read the full documentation**: Check the framework's README
491
+ 2. **Explore graph rules**: Add automation with `p_graph_rules`
492
+ 3. **Implement permissions**: Use path-based access control
493
+ 4. **Add notifications**: Set up `p_notification_path` for real-time updates
494
+ 5. **Build your UI**: Connect to any frontend framework (React, Vue, Svelte, etc.)
495
+
496
+ ## Example: Complete Todo App
497
+
498
+ **Database Setup:**
499
+ ```sql
500
+ CREATE TABLE todos (
501
+ id SERIAL PRIMARY KEY,
502
+ user_id INT NOT NULL REFERENCES users(id),
503
+ title TEXT NOT NULL,
504
+ completed BOOLEAN DEFAULT FALSE,
505
+ created_at TIMESTAMPTZ DEFAULT NOW()
506
+ );
507
+
508
+ SELECT dzql.register_entity(
509
+ 'todos',
510
+ 'title',
511
+ array['title'],
512
+ '{"user": "users"}'::jsonb
513
+ );
514
+ ```
515
+
516
+ **Server:**
517
+ ```javascript
518
+ import { createServer } from 'dzql';
519
+
520
+ const server = createServer({
521
+ port: 3000
522
+ });
523
+ ```
524
+
525
+ **Client:**
526
+ ```javascript
527
+ const ws = new WebSocketManager();
528
+ await ws.connect();
529
+ await ws.api.login_user({ email: 'user@example.com', password: 'pass' });
530
+
531
+ // Create todo
532
+ const todo = await ws.api.save.todos({
533
+ title: 'Learn DZQL',
534
+ completed: false
535
+ });
536
+
537
+ // Get all todos
538
+ const results = await ws.api.search.todos({
539
+ filters: { completed: false },
540
+ limit: 100
541
+ });
542
+
543
+ // Update todo
544
+ await ws.api.save.todos({
545
+ id: todo.id,
546
+ completed: true
547
+ });
548
+
549
+ // Delete todo
550
+ await ws.api.delete.todos({ id: todo.id });
551
+ ```
552
+
553
+ ## Support
554
+
555
+ - **GitHub**: https://github.com/blueshed/dzql
556
+ - **Issues**: https://github.com/blueshed/dzql/issues
557
+ - **Documentation**: See README.md in the package
558
+
559
+ Happy building! 🚀
package/README.md ADDED
@@ -0,0 +1,500 @@
1
+ # DZQL - Zero-Boilerplate Database Framework
2
+
3
+ PostgreSQL-powered framework with automatic CRUD operations, real-time WebSocket synchronization, and graph-based permissions. **No migrations. No schema files. No API boilerplate.**
4
+
5
+ ```bash
6
+ npm install dzql
7
+ # or with Bun (no Node.js required)
8
+ bun add dzql
9
+ ```
10
+
11
+ ## Why DZQL?
12
+
13
+ ### Before DZQL
14
+ ```javascript
15
+ // Traditional approach: Write everything
16
+ app.post('/api/users', authenticate, validate, async (req, res) => {
17
+ try {
18
+ const user = await db.query('INSERT INTO users (...) VALUES (...)', [...]);
19
+ res.json(user);
20
+ } catch (error) {
21
+ res.status(500).json({ error: error.message });
22
+ }
23
+ });
24
+ // Repeat for GET, PUT, DELETE, SEARCH, LOOKUP... = 50+ lines of boilerplate
25
+ ```
26
+
27
+ ### With DZQL
28
+ ```javascript
29
+ // That's it. All 5 operations work automatically.
30
+ // GET, SAVE, DELETE, LOOKUP, SEARCH
31
+ const user = await ws.api.save.users({ name: 'John' });
32
+ const results = await ws.api.search.users({ filters: {name: 'john'} });
33
+ ```
34
+
35
+ ## Features
36
+
37
+ ✅ **Zero Boilerplate** - Register entity, get 5 CRUD operations automatically
38
+ ✅ **Real-time WebSocket** - Automatic change notifications to all clients
39
+ ✅ **PostgreSQL-native** - Leverage full SQL power when needed
40
+ ✅ **Graph Rules** - Cascading operations without joins
41
+ ✅ **Permissions & RLS** - Row-level security built-in
42
+ ✅ **Full-text Search** - Built-in search with filters & pagination
43
+ ✅ **Type-safe** - Uses PostgreSQL as source of truth
44
+ ✅ **Framework-agnostic** - Works with any frontend (React, Vue, Svelte, plain JS)
45
+ ✅ **Bun Native** - No Node.js required
46
+
47
+ ## Quick Start
48
+
49
+ ### 1. Install
50
+ ```bash
51
+ bun add dzql
52
+ ```
53
+
54
+ ### 2. Start PostgreSQL
55
+ ```bash
56
+ docker run -d \
57
+ -e POSTGRES_PASSWORD=dzql \
58
+ -e POSTGRES_DB=dzql \
59
+ -p 5432:5432 \
60
+ postgres:latest
61
+ ```
62
+
63
+ ### 3. Create Server
64
+ ```javascript
65
+ import { createServer } from 'dzql';
66
+
67
+ const server = createServer({ port: 3000 });
68
+ console.log('🚀 Server on ws://localhost:3000/ws');
69
+ ```
70
+
71
+ ### 4. Initialize Database
72
+ ```bash
73
+ # Apply DZQL core migrations (included in package)
74
+ psql -h localhost -U postgres -d dzql < node_modules/dzql/src/database/migrations/*.sql
75
+
76
+ # Register your entities
77
+ psql -h localhost -U postgres -d dzql << EOF
78
+ CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT);
79
+
80
+ SELECT dzql.register_entity(
81
+ 'users',
82
+ 'name',
83
+ array['name', 'email']
84
+ );
85
+ EOF
86
+ ```
87
+
88
+ ### 5. Use from Client
89
+ ```javascript
90
+ import { WebSocketManager } from 'dzql/client';
91
+
92
+ const ws = new WebSocketManager();
93
+ await ws.connect();
94
+
95
+ // All 5 operations work automatically
96
+ const user = await ws.api.save.users({ name: 'Alice', email: 'alice@example.com' });
97
+ const results = await ws.api.search.users({ filters: { name: { ilike: '%alice%' } } });
98
+ const deleted = await ws.api.delete.users({ id: user.id });
99
+ ```
100
+
101
+ ## The 5 Operations
102
+
103
+ Every registered entity automatically gets these 5 operations:
104
+
105
+ ### GET - Retrieve Single Record
106
+ ```javascript
107
+ const user = await ws.api.get.users({ id: 1 });
108
+ // Server: await db.api.get.users({ id: 1 }, userId);
109
+ ```
110
+
111
+ ### SAVE - Create or Update
112
+ ```javascript
113
+ const user = await ws.api.save.users({
114
+ id: 1, // Optional - omit for insert
115
+ name: 'Alice',
116
+ email: 'alice@example.com'
117
+ });
118
+ ```
119
+
120
+ ### DELETE - Remove Record
121
+ ```javascript
122
+ const deleted = await ws.api.delete.users({ id: 1 });
123
+ ```
124
+
125
+ ### LOOKUP - Autocomplete/Label Lookup
126
+ ```javascript
127
+ const options = await ws.api.lookup.users({ p_filter: 'ali' });
128
+ // Returns: [{ label: 'Alice', value: 1 }]
129
+ ```
130
+
131
+ ### SEARCH - Advanced Search with Pagination
132
+ ```javascript
133
+ const results = await ws.api.search.users({
134
+ filters: {
135
+ name: { ilike: '%alice%' },
136
+ email: 'alice@example.com',
137
+ created_at: { gte: '2025-01-01' }
138
+ },
139
+ sort: { field: 'name', order: 'asc' },
140
+ page: 1,
141
+ limit: 25
142
+ });
143
+ // Returns: { data: [...], total: 42, page: 1, limit: 25 }
144
+ ```
145
+
146
+ ## Entity Registration
147
+
148
+ Before DZQL works with a table, register it:
149
+
150
+ ```sql
151
+ SELECT dzql.register_entity(
152
+ p_table_name := 'users',
153
+ p_label_field := 'name', -- For LOOKUP display
154
+ p_searchable_fields := array['name', 'email'], -- For SEARCH
155
+ p_fk_includes := '{"department": "departments"}'::jsonb, -- Dereference FKs
156
+ p_graph_rules := '{
157
+ "on_delete": {
158
+ "cascade": {
159
+ "actions": [{
160
+ "type": "delete",
161
+ "entity": "posts",
162
+ "condition": "user_id = @id"
163
+ }]
164
+ }
165
+ }
166
+ }'::jsonb
167
+ );
168
+ ```
169
+
170
+ ## Core API
171
+
172
+ ### Server-Side (Bun/Node)
173
+
174
+ ```javascript
175
+ import { createServer, db, sql } from 'dzql';
176
+
177
+ // Direct SQL access
178
+ const users = await sql`SELECT * FROM users WHERE active = true`;
179
+
180
+ // DZQL operations (require userId for permissions)
181
+ const user = await db.api.get.users({ id: 1 }, userId);
182
+ const saved = await db.api.save.users({ name: 'Bob' }, userId);
183
+ const searched = await db.api.search.users(
184
+ { filters: { name: 'bob' } },
185
+ userId
186
+ );
187
+ const deleted = await db.api.delete.users({ id: 1 }, userId);
188
+ const options = await db.api.lookup.users({ p_filter: 'bo' }, userId);
189
+
190
+ // Custom functions
191
+ const result = await db.api.myCustomFunction({ param: 'value' }, userId);
192
+
193
+ // Start server
194
+ const server = createServer({
195
+ port: 3000,
196
+ customApi: {}, // Optional: add custom functions
197
+ staticPath: './public' // Optional: serve static files
198
+ });
199
+ ```
200
+
201
+ ### Client-Side (Browser/Bun)
202
+
203
+ ```javascript
204
+ import { WebSocketManager } from 'dzql/client';
205
+
206
+ // Create connection
207
+ const ws = new WebSocketManager();
208
+ await ws.connect();
209
+
210
+ // Authentication
211
+ const auth = await ws.api.login_user({
212
+ email: 'user@example.com',
213
+ password: 'password'
214
+ });
215
+ // Returns: { token, profile, user_id }
216
+
217
+ // All DZQL operations
218
+ const user = await ws.api.get.users({ id: 1 });
219
+ const saved = await ws.api.save.users({ name: 'Charlie' });
220
+ const deleted = await ws.api.delete.users({ id: 1 });
221
+ const lookup = await ws.api.lookup.users({ p_filter: 'char' });
222
+ const search = await ws.api.search.users({ filters: {} });
223
+
224
+ // Custom functions
225
+ const result = await ws.api.myCustomFunction({ foo: 'bar' });
226
+
227
+ // Real-time events
228
+ const unsubscribe = ws.onBroadcast((method, params) => {
229
+ console.log(`${method}:`, params.data);
230
+ // Events: "users:insert", "users:update", "users:delete"
231
+ });
232
+
233
+ // Cleanup
234
+ ws.cleanDisconnect();
235
+ ```
236
+
237
+ ## Custom Functions
238
+
239
+ Add functions alongside DZQL operations:
240
+
241
+ ### PostgreSQL Function
242
+ ```sql
243
+ CREATE OR REPLACE FUNCTION transfer_amount(
244
+ p_user_id INT,
245
+ p_from_account INT,
246
+ p_to_account INT,
247
+ p_amount DECIMAL
248
+ ) RETURNS TABLE (success BOOLEAN, message TEXT) AS $$
249
+ BEGIN
250
+ -- Your logic here
251
+ RETURN QUERY SELECT true, 'Transfer complete';
252
+ END;
253
+ $$ LANGUAGE plpgsql;
254
+ ```
255
+
256
+ ### Bun Function
257
+ ```javascript
258
+ // server/api.js
259
+ export async function transfer_amount(userId, params) {
260
+ const { from_account, to_account, amount } = params;
261
+ // Your logic here
262
+ return { success: true, message: 'Transfer complete' };
263
+ }
264
+
265
+ // server/index.js
266
+ const customApi = await import('./api.js');
267
+ const server = createServer({ customApi });
268
+ ```
269
+
270
+ ### Usage
271
+ ```javascript
272
+ const result = await ws.api.transfer_amount({
273
+ from_account: 1,
274
+ to_account: 2,
275
+ amount: 100
276
+ });
277
+ ```
278
+
279
+ ## Real-time Events
280
+
281
+ Listen for database changes in real-time:
282
+
283
+ ```javascript
284
+ ws.onBroadcast((method, params) => {
285
+ if (method === 'users:insert') {
286
+ console.log('New user:', params.data);
287
+ // params: { op: 'insert', table: 'users', data: {...}, notify_users: [...] }
288
+ }
289
+ if (method === 'users:update') {
290
+ console.log('User updated:', params.data);
291
+ }
292
+ if (method === 'users:delete') {
293
+ console.log('User deleted:', params.data);
294
+ }
295
+ });
296
+ ```
297
+
298
+ ## Graph Rules - Cascading Operations
299
+
300
+ Automatically cascade changes through relationships:
301
+
302
+ ```sql
303
+ SELECT dzql.register_entity(
304
+ p_table_name := 'organisations',
305
+ p_label_field := 'name',
306
+ p_searchable_fields := array['name'],
307
+ p_graph_rules := '{
308
+ "on_delete": {
309
+ "cascade_to_teams": {
310
+ "actions": [{
311
+ "type": "delete",
312
+ "entity": "teams",
313
+ "condition": "org_id = @id"
314
+ }]
315
+ }
316
+ },
317
+ "on_create": {
318
+ "create_default_team": {
319
+ "actions": [{
320
+ "type": "create",
321
+ "entity": "teams",
322
+ "data": {
323
+ "org_id": "@id",
324
+ "name": "Default Team"
325
+ }
326
+ }]
327
+ }
328
+ }
329
+ }'::jsonb
330
+ );
331
+ ```
332
+
333
+ Available actions: `create`, `update`, `delete`, `insert`, `call_function`
334
+
335
+ ## Permissions & Row-Level Security
336
+
337
+ Implement permissions in your entity registration:
338
+
339
+ ```sql
340
+ SELECT dzql.register_entity(
341
+ p_table_name := 'posts',
342
+ p_label_field := 'title',
343
+ p_searchable_fields := array['title', 'content'],
344
+ p_permission_rules := '{
345
+ "view": {
346
+ "public_posts": {
347
+ "condition": "public = true OR author_id = @user_id"
348
+ }
349
+ },
350
+ "edit": {
351
+ "own_posts": {
352
+ "condition": "author_id = @user_id"
353
+ }
354
+ }
355
+ }'::jsonb
356
+ );
357
+ ```
358
+
359
+ ## Search Filter Operators
360
+
361
+ ```javascript
362
+ const results = await ws.api.search.venues({
363
+ filters: {
364
+ // Exact match
365
+ name: 'Madison Square Garden',
366
+
367
+ // Comparison operators
368
+ capacity: { gt: 1000 }, // Greater than
369
+ capacity: { gte: 1000 }, // Greater or equal
370
+ capacity: { lt: 50000 }, // Less than
371
+ capacity: { lte: 50000 }, // Less or equal
372
+ capacity: { neq: 5000 }, // Not equal
373
+
374
+ // Range
375
+ capacity: { between: [1000, 50000] },
376
+
377
+ // Pattern matching
378
+ name: { like: '%garden%' }, // Case-sensitive
379
+ name: { ilike: '%GARDEN%' }, // Case-insensitive
380
+
381
+ // NULL checks
382
+ description: null, // IS NULL
383
+ description: { not_null: true }, // IS NOT NULL
384
+
385
+ // Arrays
386
+ categories: ['sports', 'music'], // IN array
387
+ categories: { not_in: ['adult'] }, // NOT IN array
388
+
389
+ // Text search (across searchable_fields)
390
+ _search: 'madison garden'
391
+ },
392
+ page: 1,
393
+ limit: 25
394
+ });
395
+ ```
396
+
397
+ ## Project Structure
398
+
399
+ ```
400
+ my-app/
401
+ ├── server/
402
+ │ ├── index.js # Server entry point
403
+ │ └── api.js # Custom API functions (optional)
404
+ ├── database/
405
+ │ ├── docker-compose.yml # PostgreSQL setup
406
+ │ ├── init_db/
407
+ │ │ ├── 001_schema.sql # Your tables
408
+ │ │ └── 002_entities.sql # Entity registration
409
+ │ └── seeds/ # Sample data (optional)
410
+ ├── client/
411
+ │ └── index.html # Frontend (optional)
412
+ ├── tests/
413
+ │ └── app.test.js
414
+ ├── package.json
415
+ └── bunfig.toml # Bun config (optional)
416
+ ```
417
+
418
+ ## Environment Variables
419
+
420
+ ```bash
421
+ # Database
422
+ DATABASE_URL=postgresql://dzql:dzql@localhost:5432/dzql
423
+
424
+ # Server
425
+ PORT=3000
426
+ NODE_ENV=development
427
+
428
+ # JWT
429
+ JWT_SECRET=your-secret-key-min-32-chars
430
+ JWT_EXPIRES_IN=7d
431
+
432
+ # WebSocket
433
+ WS_PING_INTERVAL=30000 # Keep-alive ping (Heroku safe: <55s)
434
+ WS_PING_TIMEOUT=5000
435
+
436
+ # Logging
437
+ LOG_LEVEL=INFO # ERROR, WARN, INFO, DEBUG, TRACE
438
+ LOG_CATEGORIES=ws:debug,db:debug
439
+ ```
440
+
441
+ ## Examples
442
+
443
+ See the [venues example](https://github.com/blueshed/dzql/tree/main/packages/venues) for a complete working application.
444
+
445
+ ### Todo App
446
+ ```javascript
447
+ // Schema
448
+ CREATE TABLE todos (
449
+ id SERIAL PRIMARY KEY,
450
+ user_id INT REFERENCES users(id),
451
+ title TEXT NOT NULL,
452
+ completed BOOLEAN DEFAULT FALSE,
453
+ created_at TIMESTAMPTZ DEFAULT NOW()
454
+ );
455
+
456
+ SELECT dzql.register_entity('todos', 'title', array['title']);
457
+
458
+ // Client
459
+ const todo = await ws.api.save.todos({ title: 'Learn DZQL' });
460
+ const list = await ws.api.search.todos({
461
+ filters: { completed: false },
462
+ limit: 100
463
+ });
464
+ await ws.api.save.todos({ id: todo.id, completed: true });
465
+ await ws.api.delete.todos({ id: todo.id });
466
+ ```
467
+
468
+ ## Error Handling
469
+
470
+ ```javascript
471
+ try {
472
+ const user = await ws.api.get.users({ id: 999 });
473
+ } catch (error) {
474
+ // Common errors:
475
+ // "record not found" - Record doesn't exist
476
+ // "Permission denied: view on users" - Access denied
477
+ // "entity users not configured" - Entity not registered
478
+ // "Column foo does not exist in table users" - Invalid column
479
+ console.error(error.message);
480
+ }
481
+ ```
482
+
483
+ ## Getting Help
484
+
485
+ - **Documentation**: [GETTING_STARTED.md](./GETTING_STARTED.md)
486
+ - **GitHub**: https://github.com/blueshed/dzql
487
+ - **Issues**: https://github.com/blueshed/dzql/issues
488
+ - **Email**: support@blueshed.com
489
+
490
+ ## License
491
+
492
+ MIT - See LICENSE file
493
+
494
+ ## Authors
495
+
496
+ Created by [Blueshed](https://blueshed.com)
497
+
498
+ ---
499
+
500
+ **Ready to build?** Start with [GETTING_STARTED.md](./GETTING_STARTED.md) 🚀
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
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",
@@ -14,6 +14,7 @@
14
14
  "src/**/*.js",
15
15
  "src/database/migrations/**/*.sql",
16
16
  "README.md",
17
+ "GETTING_STARTED.md",
17
18
  "LICENSE"
18
19
  ],
19
20
  "scripts": {