dzql 0.1.0-alpha.3 → 0.1.0-alpha.4

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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  DZQL is a PostgreSQL framework that gives you **atomic real-time updates** via WebSocket. Every database change broadcasts instantly to all connected clients. Zero boilerplate.
4
4
 
5
+ > **See also:** [REFERENCE.md](REFERENCE.md) for complete API documentation | [CLAUDE.md](../../CLAUDE.md) for AI development guide
6
+
5
7
  ## The Core Pattern
6
8
 
7
9
  1. **Schema = API**: Define a table → DZQL auto-creates CRUD endpoints
package/README.md CHANGED
@@ -1,91 +1,27 @@
1
- # DZQL - Zero-Boilerplate Database Framework
1
+ # DZQL
2
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.**
3
+ PostgreSQL-powered framework with automatic CRUD operations and real-time WebSocket synchronization.
4
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
- ```
5
+ ## Documentation
26
6
 
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
- ```
7
+ All documentation is maintained in the repository root:
34
8
 
35
- ## Features
9
+ - **[README.md](../../README.md)** - Project overview and quick start
10
+ - **[GETTING_STARTED.md](GETTING_STARTED.md)** - Complete tutorial with working todo app
11
+ - **[REFERENCE.md](REFERENCE.md)** - Complete API reference
12
+ - **[CLAUDE.md](../../CLAUDE.md)** - Development guide for AI assistants
13
+ - **[Venues Example](../venues/)** - Full working application
36
14
 
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
15
+ ## Quick Install
46
16
 
47
- ## Quick Start
48
-
49
- ### 1. Install
50
17
  ```bash
51
18
  bun add dzql
19
+ # or
20
+ npm install dzql
52
21
  ```
53
22
 
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
- ```
23
+ ## Quick Example
87
24
 
88
- ### 5. Use from Client
89
25
  ```javascript
90
26
  import { WebSocketManager } from 'dzql/client';
91
27
 
@@ -93,416 +29,16 @@ const ws = new WebSocketManager();
93
29
  await ws.connect();
94
30
 
95
31
  // 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
- routes: { // Optional: standard HTTP routes
199
- '/health': () => new Response('OK')
200
- },
201
- onReady: async (broadcast) => { // Optional: routes needing broadcast
202
- return {
203
- '/mcp': createMCPRoute(broadcast) // Example: MCP integration
204
- };
205
- }
206
- });
207
- ```
208
-
209
- ### Client-Side (Browser/Bun)
210
-
211
- ```javascript
212
- import { WebSocketManager } from 'dzql/client';
213
-
214
- // Create connection
215
- const ws = new WebSocketManager();
216
- await ws.connect();
217
-
218
- // Authentication
219
- const auth = await ws.api.login_user({
220
- email: 'user@example.com',
221
- password: 'password'
222
- });
223
- // Returns: { token, profile, user_id }
224
-
225
- // All DZQL operations
226
- const user = await ws.api.get.users({ id: 1 });
227
- const saved = await ws.api.save.users({ name: 'Charlie' });
228
- const deleted = await ws.api.delete.users({ id: 1 });
229
- const lookup = await ws.api.lookup.users({ p_filter: 'char' });
230
- const search = await ws.api.search.users({ filters: {} });
231
-
232
- // Custom functions
233
- const result = await ws.api.myCustomFunction({ foo: 'bar' });
234
-
235
- // Real-time events
236
- const unsubscribe = ws.onBroadcast((method, params) => {
237
- console.log(`${method}:`, params.data);
238
- // Events: "users:insert", "users:update", "users:delete"
239
- });
240
-
241
- // Cleanup
242
- ws.cleanDisconnect();
243
- ```
244
-
245
- ## Custom Functions
246
-
247
- Add functions alongside DZQL operations:
248
-
249
- ### PostgreSQL Function
250
- ```sql
251
- CREATE OR REPLACE FUNCTION transfer_amount(
252
- p_user_id INT,
253
- p_from_account INT,
254
- p_to_account INT,
255
- p_amount DECIMAL
256
- ) RETURNS TABLE (success BOOLEAN, message TEXT) AS $$
257
- BEGIN
258
- -- Your logic here
259
- RETURN QUERY SELECT true, 'Transfer complete';
260
- END;
261
- $$ LANGUAGE plpgsql;
262
- ```
263
-
264
- ### Bun Function
265
- ```javascript
266
- // server/api.js
267
- export async function transfer_amount(userId, params) {
268
- const { from_account, to_account, amount } = params;
269
- // Your logic here
270
- return { success: true, message: 'Transfer complete' };
271
- }
272
-
273
- // server/index.js
274
- const customApi = await import('./api.js');
275
- const server = createServer({ customApi });
276
- ```
277
-
278
- ### Usage
279
- ```javascript
280
- const result = await ws.api.transfer_amount({
281
- from_account: 1,
282
- to_account: 2,
283
- amount: 100
284
- });
285
- ```
286
-
287
- ## Real-time Events
288
-
289
- Listen for database changes in real-time:
290
-
291
- ```javascript
292
- ws.onBroadcast((method, params) => {
293
- if (method === 'users:insert') {
294
- console.log('New user:', params.data);
295
- // params: { op: 'insert', table: 'users', data: {...}, notify_users: [...] }
296
- }
297
- if (method === 'users:update') {
298
- console.log('User updated:', params.data);
299
- }
300
- if (method === 'users:delete') {
301
- console.log('User deleted:', params.data);
302
- }
303
- });
304
- ```
305
-
306
- ## Graph Rules - Cascading Operations
307
-
308
- Automatically cascade changes through relationships:
309
-
310
- ```sql
311
- SELECT dzql.register_entity(
312
- p_table_name := 'organisations',
313
- p_label_field := 'name',
314
- p_searchable_fields := array['name'],
315
- p_graph_rules := '{
316
- "on_delete": {
317
- "cascade_to_teams": {
318
- "actions": [{
319
- "type": "delete",
320
- "entity": "teams",
321
- "condition": "org_id = @id"
322
- }]
323
- }
324
- },
325
- "on_create": {
326
- "create_default_team": {
327
- "actions": [{
328
- "type": "create",
329
- "entity": "teams",
330
- "data": {
331
- "org_id": "@id",
332
- "name": "Default Team"
333
- }
334
- }]
335
- }
336
- }
337
- }'::jsonb
338
- );
339
- ```
340
-
341
- Available actions: `create`, `update`, `delete`, `insert`, `call_function`
342
-
343
- ## Permissions & Row-Level Security
344
-
345
- Implement permissions in your entity registration:
346
-
347
- ```sql
348
- SELECT dzql.register_entity(
349
- p_table_name := 'posts',
350
- p_label_field := 'title',
351
- p_searchable_fields := array['title', 'content'],
352
- p_permission_rules := '{
353
- "view": {
354
- "public_posts": {
355
- "condition": "public = true OR author_id = @user_id"
356
- }
357
- },
358
- "edit": {
359
- "own_posts": {
360
- "condition": "author_id = @user_id"
361
- }
362
- }
363
- }'::jsonb
364
- );
365
- ```
366
-
367
- ## Search Filter Operators
368
-
369
- ```javascript
370
- const results = await ws.api.search.venues({
371
- filters: {
372
- // Exact match
373
- name: 'Madison Square Garden',
374
-
375
- // Comparison operators
376
- capacity: { gt: 1000 }, // Greater than
377
- capacity: { gte: 1000 }, // Greater or equal
378
- capacity: { lt: 50000 }, // Less than
379
- capacity: { lte: 50000 }, // Less or equal
380
- capacity: { neq: 5000 }, // Not equal
381
-
382
- // Range
383
- capacity: { between: [1000, 50000] },
384
-
385
- // Pattern matching
386
- name: { like: '%garden%' }, // Case-sensitive
387
- name: { ilike: '%GARDEN%' }, // Case-insensitive
388
-
389
- // NULL checks
390
- description: null, // IS NULL
391
- description: { not_null: true }, // IS NOT NULL
392
-
393
- // Arrays
394
- categories: ['sports', 'music'], // IN array
395
- categories: { not_in: ['adult'] }, // NOT IN array
396
-
397
- // Text search (across searchable_fields)
398
- _search: 'madison garden'
399
- },
400
- page: 1,
401
- limit: 25
402
- });
403
- ```
404
-
405
- ## Project Structure
406
-
407
- ```
408
- my-app/
409
- ├── server/
410
- │ ├── index.js # Server entry point
411
- │ └── api.js # Custom API functions (optional)
412
- ├── database/
413
- │ ├── docker-compose.yml # PostgreSQL setup
414
- │ ├── init_db/
415
- │ │ ├── 001_schema.sql # Your tables
416
- │ │ └── 002_entities.sql # Entity registration
417
- │ └── seeds/ # Sample data (optional)
418
- ├── client/
419
- │ └── index.html # Frontend (optional)
420
- ├── tests/
421
- │ └── app.test.js
422
- ├── package.json
423
- └── bunfig.toml # Bun config (optional)
424
- ```
425
-
426
- ## Environment Variables
427
-
428
- ```bash
429
- # Database
430
- DATABASE_URL=postgresql://dzql:dzql@localhost:5432/dzql
431
-
432
- # Server
433
- PORT=3000
434
- NODE_ENV=development
435
-
436
- # JWT
437
- JWT_SECRET=your-secret-key-min-32-chars
438
- JWT_EXPIRES_IN=7d
439
-
440
- # WebSocket
441
- WS_PING_INTERVAL=30000 # Keep-alive ping (Heroku safe: <55s)
442
- WS_PING_TIMEOUT=5000
443
-
444
- # Logging
445
- LOG_LEVEL=INFO # ERROR, WARN, INFO, DEBUG, TRACE
446
- LOG_CATEGORIES=ws:debug,db:debug
32
+ const user = await ws.api.save.users({ name: 'Alice' });
33
+ const results = await ws.api.search.users({ filters: { name: 'alice' } });
447
34
  ```
448
35
 
449
- ## Examples
450
-
451
- See the [venues example](https://github.com/blueshed/dzql/tree/main/packages/venues) for a complete working application.
452
-
453
- ### Todo App
454
- ```javascript
455
- // Schema
456
- CREATE TABLE todos (
457
- id SERIAL PRIMARY KEY,
458
- user_id INT REFERENCES users(id),
459
- title TEXT NOT NULL,
460
- completed BOOLEAN DEFAULT FALSE,
461
- created_at TIMESTAMPTZ DEFAULT NOW()
462
- );
463
-
464
- SELECT dzql.register_entity('todos', 'title', array['title']);
465
-
466
- // Client
467
- const todo = await ws.api.save.todos({ title: 'Learn DZQL' });
468
- const list = await ws.api.search.todos({
469
- filters: { completed: false },
470
- limit: 100
471
- });
472
- await ws.api.save.todos({ id: todo.id, completed: true });
473
- await ws.api.delete.todos({ id: todo.id });
474
- ```
475
-
476
- ## Error Handling
36
+ ## License
477
37
 
478
- ```javascript
479
- try {
480
- const user = await ws.api.get.users({ id: 999 });
481
- } catch (error) {
482
- // Common errors:
483
- // "record not found" - Record doesn't exist
484
- // "Permission denied: view on users" - Access denied
485
- // "entity users not configured" - Entity not registered
486
- // "Column foo does not exist in table users" - Invalid column
487
- console.error(error.message);
488
- }
489
- ```
38
+ MIT
490
39
 
491
- ## Getting Help
40
+ ## Links
492
41
 
493
- - **Documentation**: [GETTING_STARTED.md](./GETTING_STARTED.md)
494
42
  - **GitHub**: https://github.com/blueshed/dzql
495
43
  - **Issues**: https://github.com/blueshed/dzql/issues
496
- - **Email**: support@blueshed.com
497
-
498
- ## License
499
-
500
- MIT - See LICENSE file
501
-
502
- ## Authors
503
-
504
- Created by [Blueshed](https://blueshed.com)
505
-
506
- ---
507
-
508
- **Ready to build?** Start with [GETTING_STARTED.md](./GETTING_STARTED.md) 🚀
44
+ - **npm**: https://www.npmjs.com/package/dzql
package/REFERENCE.md ADDED
@@ -0,0 +1,891 @@
1
+ # DZQL API Reference
2
+
3
+ Complete API documentation for DZQL framework. For tutorials, see [GETTING_STARTED.md](GETTING_STARTED.md). For AI development guide, see [CLAUDE.md](../../CLAUDE.md).
4
+
5
+ ## Table of Contents
6
+
7
+ - [The 5 Operations](#the-5-operations)
8
+ - [Entity Registration](#entity-registration)
9
+ - [Search Operators](#search-operators)
10
+ - [Graph Rules](#graph-rules)
11
+ - [Permission & Notification Paths](#permission--notification-paths)
12
+ - [Custom Functions](#custom-functions)
13
+ - [Authentication](#authentication)
14
+ - [Real-time Events](#real-time-events)
15
+ - [Temporal Relationships](#temporal-relationships)
16
+ - [Error Messages](#error-messages)
17
+
18
+ ---
19
+
20
+ ## The 5 Operations
21
+
22
+ Every registered entity automatically gets these 5 operations via the proxy API:
23
+
24
+ ### GET - Retrieve Single Record
25
+
26
+ Fetch a single record by primary key with foreign keys dereferenced.
27
+
28
+ **Client:**
29
+ ```javascript
30
+ const record = await ws.api.get.{entity}({id: 1});
31
+ ```
32
+
33
+ **Server:**
34
+ ```javascript
35
+ const record = await db.api.get.{entity}({id: 1}, userId);
36
+ ```
37
+
38
+ **Parameters:**
39
+ | Field | Type | Required | Description |
40
+ |-------|------|----------|-------------|
41
+ | `id` | any | yes | Primary key value |
42
+ | `on_date` | string | no | Temporal filtering (ISO 8601 date) |
43
+
44
+ **Returns:** Object with all fields + dereferenced FKs
45
+
46
+ **Throws:** `"record not found"` if not exists
47
+
48
+ **Example:**
49
+ ```javascript
50
+ const venue = await ws.api.get.venues({id: 1});
51
+ // {id: 1, name: "MSG", org: {id: 3, name: "Org"}, sites: [...]}
52
+
53
+ // With temporal filtering
54
+ const historical = await ws.api.get.venues({id: 1, on_date: '2023-01-01'});
55
+ ```
56
+
57
+ ---
58
+
59
+ ### SAVE - Create or Update (Upsert)
60
+
61
+ Insert new record (no `id`) or update existing (with `id`).
62
+
63
+ **Client:**
64
+ ```javascript
65
+ const record = await ws.api.save.{entity}({...fields});
66
+ ```
67
+
68
+ **Server:**
69
+ ```javascript
70
+ const record = await db.api.save.{entity}({...fields}, userId);
71
+ ```
72
+
73
+ **Parameters:**
74
+ | Field | Type | Required | Description |
75
+ |-------|------|----------|-------------|
76
+ | `id` | any | no | Omit for insert, include for update |
77
+ | ...fields | any | varies | Entity-specific fields |
78
+
79
+ **Returns:** Created/updated record
80
+
81
+ **Behavior:**
82
+ - **No `id`**: INSERT new record
83
+ - **With `id`**: UPDATE existing record (partial update supported)
84
+ - Triggers graph rules if configured
85
+ - Generates real-time event
86
+
87
+ **Example:**
88
+ ```javascript
89
+ // Insert
90
+ const venue = await ws.api.save.venues({
91
+ name: 'Madison Square Garden',
92
+ address: 'NYC',
93
+ org_id: 1
94
+ });
95
+
96
+ // Update (partial)
97
+ const updated = await ws.api.save.venues({
98
+ id: 1,
99
+ name: 'Updated Name' // Only updates name
100
+ });
101
+ ```
102
+
103
+ ---
104
+
105
+ ### DELETE - Remove Record
106
+
107
+ Delete a record by primary key.
108
+
109
+ **Client:**
110
+ ```javascript
111
+ const result = await ws.api.delete.{entity}({id: 1});
112
+ ```
113
+
114
+ **Server:**
115
+ ```javascript
116
+ const result = await db.api.delete.{entity}({id: 1}, userId);
117
+ ```
118
+
119
+ **Parameters:**
120
+ | Field | Type | Required | Description |
121
+ |-------|------|----------|-------------|
122
+ | `id` | any | yes | Primary key value |
123
+
124
+ **Returns:** Deleted record
125
+
126
+ **Behavior:**
127
+ - Hard delete (unless soft delete configured)
128
+ - Triggers graph rules if configured
129
+ - Generates real-time event
130
+
131
+ **Example:**
132
+ ```javascript
133
+ const deleted = await ws.api.delete.venues({id: 1});
134
+ ```
135
+
136
+ ---
137
+
138
+ ### LOOKUP - Autocomplete/Typeahead
139
+
140
+ Get label-value pairs for autocomplete inputs.
141
+
142
+ **Client:**
143
+ ```javascript
144
+ const options = await ws.api.lookup.{entity}({p_filter: 'search'});
145
+ ```
146
+
147
+ **Server:**
148
+ ```javascript
149
+ const options = await db.api.lookup.{entity}({p_filter: 'search'}, userId);
150
+ ```
151
+
152
+ **Parameters:**
153
+ | Field | Type | Required | Description |
154
+ |-------|------|----------|-------------|
155
+ | `p_filter` | string | no | Search term (matches label field) |
156
+
157
+ **Returns:** Array of `{label, value}` objects
158
+
159
+ **Example:**
160
+ ```javascript
161
+ const options = await ws.api.lookup.venues({p_filter: 'madison'});
162
+ // [{label: "Madison Square Garden", value: 1}, ...]
163
+ ```
164
+
165
+ ---
166
+
167
+ ### SEARCH - Advanced Search with Pagination
168
+
169
+ Search with filters, sorting, and pagination.
170
+
171
+ **Client:**
172
+ ```javascript
173
+ const results = await ws.api.search.{entity}({
174
+ filters: {...},
175
+ sort: {field, order},
176
+ page: 1,
177
+ limit: 25
178
+ });
179
+ ```
180
+
181
+ **Server:**
182
+ ```javascript
183
+ const results = await db.api.search.{entity}({...}, userId);
184
+ ```
185
+
186
+ **Parameters:**
187
+ | Field | Type | Required | Description |
188
+ |-------|------|----------|-------------|
189
+ | `filters` | object | no | See [Search Operators](#search-operators) |
190
+ | `sort` | object | no | `{field: 'name', order: 'asc' | 'desc'}` |
191
+ | `page` | number | no | Page number (1-indexed, default: 1) |
192
+ | `limit` | number | no | Records per page (default: 25) |
193
+
194
+ **Returns:**
195
+ ```javascript
196
+ {
197
+ data: [...], // Array of records
198
+ total: 100, // Total matching records
199
+ page: 1, // Current page
200
+ limit: 25 // Records per page
201
+ }
202
+ ```
203
+
204
+ **Example:**
205
+ ```javascript
206
+ const results = await ws.api.search.venues({
207
+ filters: {
208
+ city: 'New York',
209
+ capacity: {gte: 1000, lt: 5000},
210
+ name: {ilike: '%garden%'},
211
+ _search: 'madison' // Text search across searchable fields
212
+ },
213
+ sort: {field: 'name', order: 'asc'},
214
+ page: 1,
215
+ limit: 25
216
+ });
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Entity Registration
222
+
223
+ Register an entity to enable all 5 operations via `dzql.register_entity()`.
224
+
225
+ ### Full Signature
226
+
227
+ ```sql
228
+ SELECT dzql.register_entity(
229
+ p_table_name TEXT,
230
+ p_label_field TEXT,
231
+ p_searchable_fields TEXT[],
232
+ p_fk_includes JSONB DEFAULT '{}'::jsonb,
233
+ p_soft_delete BOOLEAN DEFAULT false,
234
+ p_temporal_fields JSONB DEFAULT '{}'::jsonb,
235
+ p_notification_paths JSONB DEFAULT '{}'::jsonb,
236
+ p_permission_paths JSONB DEFAULT '{}'::jsonb,
237
+ p_graph_rules JSONB DEFAULT '{}'::jsonb
238
+ );
239
+ ```
240
+
241
+ ### Parameters
242
+
243
+ | Parameter | Type | Required | Description |
244
+ |-----------|------|----------|-------------|
245
+ | `p_table_name` | TEXT | **yes** | Table name in database |
246
+ | `p_label_field` | TEXT | **yes** | Field used for LOOKUP display |
247
+ | `p_searchable_fields` | TEXT[] | **yes** | Fields searchable by SEARCH (min: 1) |
248
+ | `p_fk_includes` | JSONB | no | Foreign keys to dereference in GET |
249
+ | `p_soft_delete` | BOOLEAN | no | Enable soft delete (default: false) |
250
+ | `p_temporal_fields` | JSONB | no | Temporal field config (valid_from/valid_to) |
251
+ | `p_notification_paths` | JSONB | no | Who receives real-time updates |
252
+ | `p_permission_paths` | JSONB | no | CRUD permission rules |
253
+ | `p_graph_rules` | JSONB | no | Automatic relationship management |
254
+
255
+ ### FK Includes
256
+
257
+ Configure which foreign keys to dereference in GET operations:
258
+
259
+ ```sql
260
+ -- Single object dereference
261
+ '{"org": "organisations"}' -- venue.org_id -> full org object
262
+
263
+ -- Child array inclusion
264
+ '{"sites": "sites"}' -- Include all child sites (auto-detects FK)
265
+
266
+ -- Multiple
267
+ '{"org": "organisations", "sites": "sites", "venue": "venues"}'
268
+ ```
269
+
270
+ **Result example:**
271
+ ```javascript
272
+ {
273
+ id: 1,
274
+ name: "Madison Square Garden",
275
+ org_id: 3,
276
+ org: {id: 3, name: "Venue Management", ...}, // Dereferenced
277
+ sites: [ // Child array
278
+ {id: 1, name: "Main Entrance", ...},
279
+ {id: 2, name: "Concourse", ...}
280
+ ]
281
+ }
282
+ ```
283
+
284
+ ### Temporal Fields
285
+
286
+ Enable temporal relationships with `valid_from`/`valid_to`:
287
+
288
+ ```sql
289
+ '{
290
+ "valid_from": "valid_from", -- Column name for start date
291
+ "valid_to": "valid_to" -- Column name for end date
292
+ }'
293
+ ```
294
+
295
+ **Usage:**
296
+ ```javascript
297
+ // Current relationships (default)
298
+ const rights = await ws.api.get.contractor_rights({id: 1});
299
+
300
+ // Historical relationships
301
+ const past = await ws.api.get.contractor_rights({id: 1, on_date: '2023-01-01'});
302
+ ```
303
+
304
+ ### Example Registration
305
+
306
+ ```sql
307
+ SELECT dzql.register_entity(
308
+ 'venues', -- table name
309
+ 'name', -- label field
310
+ array['name', 'address', 'description'], -- searchable
311
+ '{"org": "organisations", "sites": "sites"}', -- FK includes
312
+ false, -- soft delete
313
+ '{}', -- temporal (none)
314
+ '{ -- notifications
315
+ "ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"]
316
+ }',
317
+ '{ -- permissions
318
+ "create": ["@org_id->acts_for[org_id=$]{active}.user_id"],
319
+ "update": ["@org_id->acts_for[org_id=$]{active}.user_id"],
320
+ "delete": ["@org_id->acts_for[org_id=$]{active}.user_id"],
321
+ "view": []
322
+ }',
323
+ '{ -- graph rules
324
+ "on_create": {
325
+ "establish_site": {
326
+ "description": "Create default site",
327
+ "actions": [{
328
+ "type": "create",
329
+ "entity": "sites",
330
+ "data": {"name": "Main Site", "venue_id": "@id"}
331
+ }]
332
+ }
333
+ }
334
+ }'
335
+ );
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Search Operators
341
+
342
+ The SEARCH operation supports advanced filtering via the `filters` object.
343
+
344
+ ### Operator Reference
345
+
346
+ | Operator | Syntax | Description | Example |
347
+ |----------|--------|-------------|---------|
348
+ | **Exact match** | `field: value` | Equality | `{name: 'Alice'}` |
349
+ | **Greater than** | `{gt: n}` | `>` | `{age: {gt: 18}}` |
350
+ | **Greater or equal** | `{gte: n}` | `>=` | `{age: {gte: 18}}` |
351
+ | **Less than** | `{lt: n}` | `<` | `{age: {lt: 65}}` |
352
+ | **Less or equal** | `{lte: n}` | `<=` | `{age: {lte: 65}}` |
353
+ | **Not equal** | `{neq: v}` | `!=` | `{status: {neq: 'deleted'}}` |
354
+ | **Between** | `{between: [a, b]}` | `BETWEEN a AND b` | `{age: {between: [18, 65]}}` |
355
+ | **LIKE** | `{like: 'pattern'}` | Case-sensitive pattern | `{name: {like: '%Garden%'}}` |
356
+ | **ILIKE** | `{ilike: 'pattern'}` | Case-insensitive pattern | `{name: {ilike: '%garden%'}}` |
357
+ | **IS NULL** | `field: null` | NULL check | `{description: null}` |
358
+ | **IS NOT NULL** | `{not_null: true}` | NOT NULL check | `{description: {not_null: true}}` |
359
+ | **IN array** | `field: [...]` | `IN (...)` | `{city: ['NYC', 'LA']}` |
360
+ | **NOT IN array** | `{not_in: [...]}` | `NOT IN (...)` | `{status: {not_in: ['deleted']}}` |
361
+ | **Text search** | `_search: 'terms'` | Across searchable fields | `{_search: 'madison garden'}` |
362
+
363
+ ### Complete Example
364
+
365
+ ```javascript
366
+ const results = await ws.api.search.venues({
367
+ filters: {
368
+ // Exact match
369
+ city: 'New York',
370
+
371
+ // Comparison
372
+ capacity: {gte: 1000, lt: 5000},
373
+
374
+ // Pattern matching
375
+ name: {ilike: '%garden%'},
376
+
377
+ // NULL checks
378
+ description: {not_null: true},
379
+
380
+ // Arrays
381
+ categories: ['sports', 'music'],
382
+ status: {not_in: ['deleted', 'closed']},
383
+
384
+ // Text search (across all searchable_fields)
385
+ _search: 'madison square'
386
+ },
387
+ sort: {field: 'capacity', order: 'desc'},
388
+ page: 1,
389
+ limit: 25
390
+ });
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Graph Rules
396
+
397
+ Automatically manage entity relationships when data changes.
398
+
399
+ ### Structure
400
+
401
+ ```jsonb
402
+ {
403
+ "on_create": {
404
+ "rule_name": {
405
+ "description": "Human-readable description",
406
+ "actions": [
407
+ {
408
+ "type": "create|update|delete",
409
+ "entity": "target_table",
410
+ "data": {"field": "@variable"}, // for create/update
411
+ "match": {"field": "@variable"} // for update/delete
412
+ }
413
+ ]
414
+ }
415
+ },
416
+ "on_update": { /* same structure */ },
417
+ "on_delete": { /* same structure */ }
418
+ }
419
+ ```
420
+
421
+ ### Action Types
422
+
423
+ | Type | Fields | Description |
424
+ |------|--------|-------------|
425
+ | `create` | `entity`, `data` | INSERT new record |
426
+ | `update` | `entity`, `match`, `data` | UPDATE matching records |
427
+ | `delete` | `entity`, `match` | DELETE matching records |
428
+
429
+ ### Variables
430
+
431
+ Variables reference data from the triggering operation:
432
+
433
+ | Variable | Description | Example |
434
+ |----------|-------------|---------|
435
+ | `@user_id` | Current authenticated user | `"created_by": "@user_id"` |
436
+ | `@id` | Primary key of the record | `"org_id": "@id"` |
437
+ | `@field_name` | Any field from the record | `"org_id": "@org_id"` |
438
+ | `@now` | Current timestamp | `"created_at": "@now"` |
439
+ | `@today` | Current date | `"valid_from": "@today"` |
440
+
441
+ ### Common Patterns
442
+
443
+ #### Creator Becomes Owner
444
+ ```jsonb
445
+ {
446
+ "on_create": {
447
+ "establish_ownership": {
448
+ "description": "Creator becomes member of organisation",
449
+ "actions": [{
450
+ "type": "create",
451
+ "entity": "acts_for",
452
+ "data": {
453
+ "user_id": "@user_id",
454
+ "org_id": "@id",
455
+ "valid_from": "@today"
456
+ }
457
+ }]
458
+ }
459
+ }
460
+ }
461
+ ```
462
+
463
+ #### Cascade Delete
464
+ ```jsonb
465
+ {
466
+ "on_delete": {
467
+ "cascade_venues": {
468
+ "description": "Delete all venues when org is deleted",
469
+ "actions": [{
470
+ "type": "delete",
471
+ "entity": "venues",
472
+ "match": {"org_id": "@id"}
473
+ }]
474
+ }
475
+ }
476
+ }
477
+ ```
478
+
479
+ #### Temporal Transition
480
+ ```jsonb
481
+ {
482
+ "on_create": {
483
+ "expire_previous": {
484
+ "description": "End previous temporal relationship",
485
+ "actions": [{
486
+ "type": "update",
487
+ "entity": "acts_for",
488
+ "match": {
489
+ "user_id": "@user_id",
490
+ "org_id": "@org_id",
491
+ "valid_to": null
492
+ },
493
+ "data": {
494
+ "valid_to": "@valid_from"
495
+ }
496
+ }]
497
+ }
498
+ }
499
+ }
500
+ ```
501
+
502
+ ### Execution
503
+
504
+ - **Atomic**: All rules execute in the same transaction
505
+ - **Sequential**: Actions execute in order within each rule
506
+ - **Rollback**: If any action fails, entire transaction rolls back
507
+ - **Events**: Each action generates its own audit event
508
+
509
+ ---
510
+
511
+ ## Permission & Notification Paths
512
+
513
+ Paths use a unified syntax for both permissions and notifications.
514
+
515
+ ### Path Syntax
516
+
517
+ ```
518
+ @field->table[filter]{temporal}.target_field
519
+ ```
520
+
521
+ **Components:**
522
+ - `@field` - Start from a field in the current record
523
+ - `->table` - Navigate to related table
524
+ - `[filter]` - WHERE clause (`$` = current field value)
525
+ - `{temporal}` - Apply temporal filtering (`{active}` = valid now)
526
+ - `.target_field` - Extract this field as result
527
+
528
+ ### Permission Paths
529
+
530
+ Control who can perform CRUD operations:
531
+
532
+ ```sql
533
+ '{
534
+ "create": ["@org_id->acts_for[org_id=$]{active}.user_id"],
535
+ "update": ["@org_id->acts_for[org_id=$]{active}.user_id"],
536
+ "delete": ["@org_id->acts_for[org_id=$]{active}.user_id"],
537
+ "view": [] -- Empty array = public access
538
+ }'
539
+ ```
540
+
541
+ **Permission types:**
542
+ - `create` - Who can create records
543
+ - `update` - Who can modify records
544
+ - `delete` - Who can remove records
545
+ - `view` - Who can read records (empty = public)
546
+
547
+ **Behavior:**
548
+ - User's `user_id` must be in resolved set of user_ids
549
+ - Checked before operation executes
550
+ - Empty array = allow all
551
+ - Missing permission type = deny all
552
+
553
+ ### Notification Paths
554
+
555
+ Determine who receives real-time updates:
556
+
557
+ ```sql
558
+ '{
559
+ "ownership": ["@org_id->acts_for[org_id=$]{active}.user_id"],
560
+ "sponsorship": ["@sponsor_org_id->acts_for[org_id=$]{active}.user_id"]
561
+ }'
562
+ ```
563
+
564
+ **Behavior:**
565
+ - Resolves to array of user_ids or `null`
566
+ - `null` = broadcast to all authenticated users
567
+ - Array = send only to specified users
568
+ - Multiple paths = union of all resolved user_ids
569
+
570
+ ### Path Examples
571
+
572
+ ```sql
573
+ -- Direct user reference
574
+ '@user_id'
575
+
576
+ -- Via organization
577
+ '@org_id->acts_for[org_id=$]{active}.user_id'
578
+
579
+ -- Via nested relationship
580
+ '@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id'
581
+
582
+ -- Via multiple relationships
583
+ '@package_id->packages.owner_org_id->acts_for[org_id=$]{active}.user_id'
584
+ ```
585
+
586
+ ---
587
+
588
+ ## Custom Functions
589
+
590
+ Extend DZQL with custom PostgreSQL or Bun functions.
591
+
592
+ ### PostgreSQL Functions
593
+
594
+ Create stored procedures and call via proxy API:
595
+
596
+ ```sql
597
+ CREATE OR REPLACE FUNCTION my_function(
598
+ p_user_id INT, -- REQUIRED: First parameter
599
+ p_param TEXT DEFAULT 'default'
600
+ ) RETURNS JSONB
601
+ LANGUAGE plpgsql
602
+ SECURITY DEFINER
603
+ AS $$
604
+ BEGIN
605
+ -- Your logic here
606
+ RETURN jsonb_build_object('result', p_param);
607
+ END;
608
+ $$;
609
+ ```
610
+
611
+ **Call:**
612
+ ```javascript
613
+ const result = await ws.api.my_function({param: 'value'});
614
+ ```
615
+
616
+ **Conventions:**
617
+ - First parameter **must** be `p_user_id INT`
618
+ - Can access full PostgreSQL ecosystem
619
+ - Automatically transactional
620
+ - Optional registration in `dzql.registry`
621
+
622
+ ### Bun Functions
623
+
624
+ Create JavaScript functions in server:
625
+
626
+ ```javascript
627
+ // server/api.js
628
+ export async function myBunFunction(userId, params = {}) {
629
+ const { param = 'default' } = params;
630
+
631
+ // Can use db.api for database access
632
+ // const data = await db.api.get.users({id: userId}, userId);
633
+
634
+ return { result: param };
635
+ }
636
+ ```
637
+
638
+ **Server setup:**
639
+ ```javascript
640
+ import * as customApi from './server/api.js';
641
+ const server = createServer({ customApi });
642
+ ```
643
+
644
+ **Call:**
645
+ ```javascript
646
+ const result = await ws.api.myBunFunction({param: 'value'});
647
+ ```
648
+
649
+ **Conventions:**
650
+ - First parameter is `userId` (number)
651
+ - Second parameter is `params` object
652
+ - Can access `db.api.*` operations
653
+ - Can use any npm packages
654
+ - Return JSON-serializable data
655
+
656
+ ### Function Comparison
657
+
658
+ | Feature | PostgreSQL | Bun |
659
+ |---------|-----------|-----|
660
+ | **Language** | SQL/PL/pgSQL | JavaScript |
661
+ | **Access to** | Database only | Database + npm ecosystem |
662
+ | **Transaction** | Automatic | Manual (via db.api) |
663
+ | **Performance** | Faster (no network) | Slower (WebSocket overhead) |
664
+ | **Use case** | Data-heavy operations | Complex business logic |
665
+
666
+ ---
667
+
668
+ ## Authentication
669
+
670
+ JWT-based authentication with automatic user_id injection.
671
+
672
+ ### Register User
673
+
674
+ ```javascript
675
+ const result = await ws.api.register_user({
676
+ email: 'user@example.com',
677
+ password: 'secure-password'
678
+ });
679
+ ```
680
+
681
+ **Returns:**
682
+ ```javascript
683
+ {
684
+ user_id: 1,
685
+ email: 'user@example.com',
686
+ token: 'eyJ...',
687
+ profile: {...}
688
+ }
689
+ ```
690
+
691
+ ### Login
692
+
693
+ ```javascript
694
+ const result = await ws.api.login_user({
695
+ email: 'user@example.com',
696
+ password: 'password'
697
+ });
698
+ ```
699
+
700
+ **Returns:** Same as register
701
+
702
+ ### Logout
703
+
704
+ ```javascript
705
+ await ws.api.logout();
706
+ ```
707
+
708
+ ### Token Storage
709
+
710
+ ```javascript
711
+ // Save token
712
+ localStorage.setItem('dzql_token', result.token);
713
+
714
+ // Auto-connect with token
715
+ const ws = new WebSocketManager();
716
+ await ws.connect(); // Automatically uses token from localStorage
717
+ ```
718
+
719
+ ### User ID Injection
720
+
721
+ - **Client**: `user_id` automatically injected from JWT
722
+ - **Server**: `user_id` must be passed explicitly as second parameter
723
+
724
+ ```javascript
725
+ // Client
726
+ const user = await ws.api.get.users({id: 1}); // userId auto-injected
727
+
728
+ // Server
729
+ const user = await db.api.get.users({id: 1}, userId); // userId explicit
730
+ ```
731
+
732
+ ---
733
+
734
+ ## Real-time Events
735
+
736
+ All database changes trigger WebSocket events.
737
+
738
+ ### Event Flow
739
+
740
+ 1. Database trigger fires on INSERT/UPDATE/DELETE
741
+ 2. Notification paths resolve affected user_ids
742
+ 3. Event written to `dzql.events` table
743
+ 4. PostgreSQL NOTIFY on 'dzql' channel
744
+ 5. Bun server filters by `notify_users`
745
+ 6. WebSocket message sent to affected clients
746
+
747
+ ### Listening for Events
748
+
749
+ ```javascript
750
+ const unsubscribe = ws.onBroadcast((method, params) => {
751
+ console.log(`Event: ${method}`, params);
752
+ });
753
+
754
+ // Stop listening
755
+ unsubscribe();
756
+ ```
757
+
758
+ ### Event Format
759
+
760
+ **Method:** `"{table}:{operation}"`
761
+ - Examples: `"users:insert"`, `"venues:update"`, `"sites:delete"`
762
+
763
+ **Params:**
764
+ ```javascript
765
+ {
766
+ table: 'venues',
767
+ op: 'insert' | 'update' | 'delete',
768
+ pk: {id: 1}, // Primary key
769
+ before: {...}, // Old values (null for insert)
770
+ after: {...}, // New values (null for delete)
771
+ user_id: 123, // Who made the change
772
+ at: '2025-01-01T...', // Timestamp
773
+ notify_users: [1, 2] // Who to notify (null = all)
774
+ }
775
+ ```
776
+
777
+ ### Event Handling Pattern
778
+
779
+ ```javascript
780
+ ws.onBroadcast((method, params) => {
781
+ const data = params.after || params.before;
782
+
783
+ if (method === 'todos:insert') {
784
+ state.todos.push(data);
785
+ } else if (method === 'todos:update') {
786
+ const idx = state.todos.findIndex(t => t.id === data.id);
787
+ if (idx !== -1) state.todos[idx] = data;
788
+ } else if (method === 'todos:delete') {
789
+ state.todos = state.todos.filter(t => t.id !== data.id);
790
+ }
791
+
792
+ render();
793
+ });
794
+ ```
795
+
796
+ ---
797
+
798
+ ## Temporal Relationships
799
+
800
+ Handle time-based relationships with `valid_from`/`valid_to` fields.
801
+
802
+ ### Configuration
803
+
804
+ ```sql
805
+ SELECT dzql.register_entity(
806
+ 'contractor_rights',
807
+ 'contractor_name',
808
+ array['contractor_name'],
809
+ '{"contractor_org": "organisations", "venue": "venues"}',
810
+ false,
811
+ '{
812
+ "valid_from": "valid_from",
813
+ "valid_to": "valid_to"
814
+ }',
815
+ '{}', '{}'
816
+ );
817
+ ```
818
+
819
+ ### Usage
820
+
821
+ ```javascript
822
+ // Get current relationships (default)
823
+ const rights = await ws.api.get.contractor_rights({id: 1});
824
+
825
+ // Get historical relationships
826
+ const past = await ws.api.get.contractor_rights({
827
+ id: 1,
828
+ on_date: '2023-01-01'
829
+ });
830
+ ```
831
+
832
+ ### Path Syntax with Temporal
833
+
834
+ ```sql
835
+ -- Current relationships only
836
+ '@org_id->acts_for[org_id=$]{active}.user_id'
837
+
838
+ -- All relationships (past and present)
839
+ '@org_id->acts_for[org_id=$].user_id'
840
+ ```
841
+
842
+ ---
843
+
844
+ ## Error Messages
845
+
846
+ Common error messages and their meanings:
847
+
848
+ | Error | Cause | Solution |
849
+ |-------|-------|----------|
850
+ | `"record not found"` | GET on non-existent ID | Check ID exists, handle 404 |
851
+ | `"Permission denied: view on users"` | User not in permission path | Check permissions, authenticate |
852
+ | `"entity users not configured"` | Entity not registered | Call `dzql.register_entity()` |
853
+ | `"Column foo does not exist in table users"` | Invalid filter field | Check searchable_fields config |
854
+ | `"Invalid function name: foo"` | Function doesn't exist | Create function or check spelling |
855
+ | `"Function not found"` | Custom function not registered | Export from api.js or create SQL function |
856
+ | `"Authentication required"` | Not logged in | Call `login_user()` first |
857
+ | `"Invalid token"` | Expired/invalid JWT | Re-authenticate |
858
+
859
+ ---
860
+
861
+ ## Server-Side API
862
+
863
+ For backend/Bun scripts, use `db.api`:
864
+
865
+ ```javascript
866
+ import { db, sql } from 'dzql';
867
+
868
+ // Direct SQL queries
869
+ const users = await sql`SELECT * FROM users WHERE active = true`;
870
+
871
+ // DZQL operations (require explicit userId)
872
+ const user = await db.api.get.users({id: 1}, userId);
873
+ const saved = await db.api.save.users({name: 'John'}, userId);
874
+ const results = await db.api.search.users({filters: {}}, userId);
875
+ const deleted = await db.api.delete.users({id: 1}, userId);
876
+ const options = await db.api.lookup.users({p_filter: 'jo'}, userId);
877
+
878
+ // Custom functions
879
+ const result = await db.api.myCustomFunction({param: 'value'}, userId);
880
+ ```
881
+
882
+ **Key difference:** Server-side requires explicit `userId` as second parameter; client-side auto-injects from JWT.
883
+
884
+ ---
885
+
886
+ ## See Also
887
+
888
+ - [GETTING_STARTED.md](GETTING_STARTED.md) - Hands-on tutorial
889
+ - [CLAUDE.md](../../CLAUDE.md) - AI development guide
890
+ - [README.md](../../README.md) - Project overview
891
+ - [Venues Example](../venues/) - Complete working application
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-alpha.4",
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,7 @@
15
15
  "src/database/migrations/**/*.sql",
16
16
  "README.md",
17
17
  "GETTING_STARTED.md",
18
+ "REFERENCE.md",
18
19
  "LICENSE"
19
20
  ],
20
21
  "scripts": {