dzql 0.4.2 โ†’ 0.4.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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ PostgreSQL-powered framework with automatic CRUD operations, live query subscrip
7
7
  - **[Documentation Hub](docs/)** - Complete documentation index
8
8
  - **[Getting Started Tutorial](docs/getting-started/tutorial.md)** - Complete tutorial with working todo app
9
9
  - **[API Reference](docs/reference/api.md)** - Complete API documentation
10
- - **[Live Query Subscriptions](docs/getting-started/subscriptions-quick-start.md)** - Real-time denormalized documents (NEW in v0.2.0)
10
+ - **[Live Query Subscriptions](docs/getting-started/subscriptions-quick-start.md)** - Real-time denormalized documents
11
11
  - **[Compiler Documentation](docs/compiler/)** - Entity compilation guide and coding standards
12
12
  - **[Claude Guide](docs/for-ai/claude-guide.md)** - Development guide for AI assistants
13
13
  - **[Venues Example](../venues/)** - Full working application
@@ -32,7 +32,7 @@ await ws.connect();
32
32
  const user = await ws.api.save.users({ name: 'Alice' });
33
33
  const results = await ws.api.search.users({ filters: { name: 'alice' } });
34
34
 
35
- // NEW in v0.2.0: Live query subscriptions
35
+ // Live query subscriptions
36
36
  const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
37
37
  { venue_id: 123 },
38
38
  (updated) => console.log('Venue changed!', updated)
@@ -59,17 +59,20 @@ See **[Compiler Documentation](docs/compiler/)** for complete usage guide, codin
59
59
  ## Testing
60
60
 
61
61
  ```bash
62
- # Start test database
63
- cd tests/test-utils && docker compose up -d
62
+ # From repository root - start test database
63
+ docker compose up -d
64
+
65
+ # Initialize test database
66
+ bun run test:init
64
67
 
65
68
  # Run tests
66
69
  bun test
67
70
 
68
71
  # Stop database
69
- cd tests/test-utils && docker compose down
72
+ docker compose down
70
73
  ```
71
74
 
72
- All tests use `bun:test` framework with automatic database setup/teardown. See **[tests/test-utils/README.md](tests/test-utils/README.md)** for details.
75
+ All tests use `bun:test` framework. See **[tests/README.md](../../tests/README.md)** for details.
73
76
 
74
77
  ## License
75
78
 
package/docs/README.md CHANGED
@@ -7,6 +7,7 @@ Complete documentation for the DZQL PostgreSQL-powered framework.
7
7
  New to DZQL? Start here:
8
8
 
9
9
  - **[Tutorial](getting-started/tutorial.md)** - Complete step-by-step guide with a working todo app
10
+ - **[Interpreter vs Compiler](guides/interpreter-vs-compiler.md)** - Understand the two execution modes
10
11
  - **[Subscriptions Quick Start](getting-started/subscriptions-quick-start.md)** - Get real-time subscriptions working in 5 minutes
11
12
 
12
13
  ## ๐Ÿ“– Guides
@@ -14,6 +15,9 @@ New to DZQL? Start here:
14
15
  Feature-specific guides and how-tos:
15
16
 
16
17
  - **[Live Query Subscriptions](guides/subscriptions.md)** - Real-time denormalized documents
18
+ - **[Many-to-Many Relationships](guides/many-to-many.md)** - Junction table management
19
+ - **[Field Defaults](guides/field-defaults.md)** - Auto-populate fields on create
20
+ - **[Custom Functions](guides/custom-functions.md)** - Extend with PostgreSQL or Bun functions
17
21
  - **[Client Stores](guides/client-stores.md)** - Pinia store patterns for Vue.js
18
22
 
19
23
  ## ๐Ÿ“˜ Reference
@@ -29,7 +33,7 @@ Complete API documentation:
29
33
  - [Quickstart](compiler/QUICKSTART.md) - Get started with the DZQL compiler
30
34
  - [Advanced Filters](compiler/ADVANCED_FILTERS.md) - Complex search operators
31
35
  - [Coding Standards](compiler/CODING_STANDARDS.md) - Best practices for DZQL code
32
- - [Comparison](compiler/COMPARISON.md) - DZQL vs other approaches
36
+ - [Comparison](compiler/COMPARISON.md) - Runtime vs compiled side-by-side
33
37
 
34
38
  ## ๐Ÿค– For AI Assistants
35
39
 
@@ -40,28 +44,6 @@ Complete API documentation:
40
44
  - [npm Package](https://www.npmjs.com/package/dzql)
41
45
  - [GitHub Repository](https://github.com/blueshed/dzql)
42
46
  - [Issue Tracker](https://github.com/blueshed/dzql/issues)
43
- - [Changelog](../../../CHANGELOG.md)
44
- - [Contributing](../../../CONTRIBUTING.md)
45
-
46
- ## ๐Ÿ—๏ธ Architecture
47
-
48
- Looking for architecture and design docs? See the [repository docs](../../../docs/):
49
-
50
- - [Permissions System](../../../docs/architecture/PERMISSIONS.md)
51
- - [Project Roadmap](../../../docs/architecture/ROADMAP.md)
52
- - [Subscription Architecture](../../../docs/architecture/SUBSCRIPTIONS_STRATEGY.md)
53
-
54
- ## ๐Ÿงช Development
55
-
56
- Contributing to DZQL? See development documentation:
57
-
58
- - [TDD Workflow](../../../docs/development/TDD_WORKFLOW.md)
59
- - [WebSocket Testing](../../../docs/development/WEBSOCKET_TESTING.md)
60
- - [Claude Web Setup](../../../docs/development/CLAUDE-WEB.md)
61
-
62
- ## ๐Ÿ“ฆ Package Contents
63
-
64
- This documentation is published with the npm package. For repository-wide documentation (contributors, development workflow, architecture), see [`/docs/`](../../../docs/) in the repository root.
65
47
 
66
48
  ## Need Help?
67
49
 
@@ -2,6 +2,38 @@
2
2
 
3
3
  This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
4
 
5
+ ## Quick Reference Card
6
+
7
+ ```
8
+ DZQL QUICK REFERENCE
9
+ ====================
10
+
11
+ 5 Operations: get, save, delete, lookup, search
12
+ 2 Modes: Interpreter (runtime) | Compiler (static SQL)
13
+ Client API: ws.api.{operation}.{entity}(params)
14
+ Server API: db.api.{operation}.{entity}(params, userId)
15
+
16
+ Entity Registration:
17
+ dzql.register_entity(
18
+ table_name, -- 'todos'
19
+ label_field, -- 'title' (for lookups)
20
+ searchable_fields, -- ARRAY['title', 'description']
21
+ fk_includes, -- '{"org": "organisations"}'
22
+ soft_delete, -- false
23
+ temporal_fields, -- '{}'
24
+ notification_paths, -- '{"ownership": ["@org_id->acts_for..."]}'
25
+ permission_paths, -- '{"view": [], "create": [...]}'
26
+ graph_rules, -- '{"on_create": {...}, "many_to_many": {...}}'
27
+ field_defaults -- '{"owner_id": "@user_id"}'
28
+ )
29
+
30
+ M2M id_field naming: tag_ids (singular + _ids), NOT tags_ids
31
+ Permission [] = public, omitted = denied
32
+ Path syntax: @field->table[filter]{temporal}.target_field
33
+
34
+ Compile: dzql compile entities.sql -o compiled/
35
+ ```
36
+
5
37
  ## Project Overview
6
38
 
7
39
  DZQL is a PostgreSQL-powered framework that eliminates CRUD boilerplate by providing automatic database operations, real-time WebSocket synchronization, and graph-based relationship management. The core concept: register an entity in PostgreSQL and instantly get 5 standard operations (get, save, delete, lookup, search) plus real-time notifications with zero code.
@@ -0,0 +1,237 @@
1
+ # Interpreter vs Compiler Mode
2
+
3
+ DZQL offers two execution modes for your entities. Understanding when to use each is fundamental to getting the best out of the framework.
4
+
5
+ ## Quick Summary
6
+
7
+ | Aspect | Interpreter | Compiler |
8
+ |--------|-------------|----------|
9
+ | **Setup** | Register entity, use immediately | Register entity, compile, deploy SQL |
10
+ | **Performance** | ~8-12ms per operation | ~2-4ms per operation |
11
+ | **Debugging** | Opaque (dynamic SQL) | Transparent (static SQL) |
12
+ | **Best For** | Development, prototyping | Production, performance-critical |
13
+
14
+ ## How It Works
15
+
16
+ ### Interpreter Mode (Runtime)
17
+
18
+ Entity configuration is stored as JSON in `dzql.entities` table and parsed at runtime:
19
+
20
+ ```
21
+ Client Request โ†’ generic_exec() โ†’ Parse JSON config โ†’ Build SQL โ†’ Execute
22
+ ```
23
+
24
+ **Characteristics:**
25
+ - Zero build step - changes take effect immediately
26
+ - JSON config parsed on every request
27
+ - Dynamic SQL generated at runtime
28
+ - Generic query plans (harder to optimize)
29
+
30
+ **Usage:**
31
+ ```sql
32
+ -- Register entity
33
+ SELECT dzql.register_entity('todos', 'title', ARRAY['title'], ...);
34
+
35
+ -- Use immediately via generic executor
36
+ SELECT dzql.generic_exec('save', 'todos', '{"title": "Buy milk"}'::jsonb, 1);
37
+ ```
38
+
39
+ ### Compiler Mode (Static)
40
+
41
+ Entity configuration is compiled into dedicated PostgreSQL functions:
42
+
43
+ ```
44
+ Entity Definition โ†’ dzql compile โ†’ Static SQL Functions โ†’ Deploy โ†’ Execute
45
+ ```
46
+
47
+ **Characteristics:**
48
+ - Build step required
49
+ - No JSON parsing at runtime
50
+ - Static SQL with specific query plans
51
+ - PostgreSQL can optimize and cache plans
52
+
53
+ **Usage:**
54
+ ```bash
55
+ # Compile entities to SQL
56
+ dzql compile entities.sql -o compiled/
57
+
58
+ # Deploy to database
59
+ psql < compiled/entities.sql
60
+ ```
61
+
62
+ ```sql
63
+ -- Use compiled functions directly
64
+ SELECT save_todos('{"title": "Buy milk"}'::jsonb, 1);
65
+ ```
66
+
67
+ ## The Server Automatically Chooses
68
+
69
+ The DZQL server (`db.js`) automatically tries compiled functions first:
70
+
71
+ ```javascript
72
+ // In callDZQLOperation()
73
+ try {
74
+ // Try compiled function: save_todos()
75
+ const result = await sql.unsafe(`SELECT save_todos($1, $2)`, [data, userId]);
76
+ return result[0].result;
77
+ } catch (error) {
78
+ // If compiled function doesn't exist, fall back to interpreter
79
+ if (error.message.includes('save_todos') && error.code === '42883') {
80
+ return await sql`SELECT dzql.generic_exec('save', 'todos', ${data}, ${userId})`;
81
+ }
82
+ throw error;
83
+ }
84
+ ```
85
+
86
+ This means you can:
87
+ 1. Start with interpreter mode during development
88
+ 2. Compile and deploy when ready for production
89
+ 3. Mix and match - some entities compiled, others interpreted
90
+
91
+ ## Performance Comparison
92
+
93
+ ### Interpreter (Runtime Parsing)
94
+
95
+ ```sql
96
+ SELECT dzql.generic_exec('save', 'venues', '{"name": "MSG"}'::jsonb, 42);
97
+ ```
98
+
99
+ **Execution steps:**
100
+ 1. Fetch entity config from `dzql.entities` (table lookup)
101
+ 2. Parse `permission_paths` JSONB
102
+ 3. Build permission query dynamically
103
+ 4. Parse `graph_rules` JSONB
104
+ 5. Execute rules via dynamic SQL
105
+ 6. Parse `notification_paths` JSONB
106
+ 7. Resolve paths dynamically
107
+ 8. Execute the actual save
108
+
109
+ **Cost:** ~8-12ms, 3-5 JSONB parses, unpredictable query plans
110
+
111
+ ### Compiler (Pre-built Functions)
112
+
113
+ ```sql
114
+ SELECT save_venues('{"name": "MSG"}'::jsonb, 42);
115
+ ```
116
+
117
+ **Execution steps:**
118
+ 1. Call `can_update_venues()` - pre-compiled permission check
119
+ 2. Execute INSERT/UPDATE - direct SQL
120
+ 3. Call `graph_venues_on_create()` - pre-compiled graph rules
121
+ 4. Call `resolve_notification_paths_venues()` - pre-compiled
122
+ 5. Done
123
+
124
+ **Cost:** ~2-4ms, 0 JSONB parses, optimized query plans
125
+
126
+ ## When to Use Each
127
+
128
+ ### Use Interpreter When:
129
+ - Rapid prototyping and development
130
+ - Schema changes frequently
131
+ - Learning DZQL concepts
132
+ - Small applications with low traffic
133
+ - Need maximum flexibility
134
+
135
+ ### Use Compiler When:
136
+ - Production deployments
137
+ - Performance is critical
138
+ - Need predictable query performance
139
+ - Want reviewable/auditable SQL
140
+ - Large teams (generated SQL is easy to review)
141
+ - Complex permission or graph rules
142
+
143
+ ### Recommended Workflow:
144
+ 1. **Development:** Use interpreter for fast iteration
145
+ 2. **Staging:** Compile and test performance
146
+ 3. **Production:** Deploy compiled functions
147
+
148
+ ## Compiling Entities
149
+
150
+ ### Via CLI
151
+
152
+ ```bash
153
+ # Single file
154
+ dzql compile database/entities.sql -o compiled/
155
+
156
+ # Multiple files
157
+ dzql compile database/*.sql -o compiled/
158
+ ```
159
+
160
+ ### Programmatically
161
+
162
+ ```javascript
163
+ import { DZQLCompiler } from 'dzql/compiler';
164
+
165
+ const compiler = new DZQLCompiler();
166
+ const result = compiler.compileFromSQL(sqlContent);
167
+
168
+ console.log(result.sql); // Generated PostgreSQL functions
169
+ ```
170
+
171
+ ### What Gets Generated
172
+
173
+ For each entity, the compiler generates:
174
+
175
+ | Function | Purpose |
176
+ |----------|---------|
177
+ | `get_{entity}(id, user_id)` | Retrieve single record |
178
+ | `save_{entity}(data, user_id)` | Create or update |
179
+ | `delete_{entity}(id, user_id)` | Delete record |
180
+ | `lookup_{entity}(term, user_id)` | Autocomplete search |
181
+ | `search_{entity}(filters, user_id)` | Paginated search |
182
+ | `can_view_{entity}(user_id, record)` | Permission check |
183
+ | `can_create_{entity}(user_id, record)` | Permission check |
184
+ | `can_update_{entity}(user_id, record)` | Permission check |
185
+ | `can_delete_{entity}(user_id, record)` | Permission check |
186
+
187
+ ## Debugging
188
+
189
+ ### Interpreter Mode
190
+
191
+ Debugging is harder because SQL is generated dynamically:
192
+
193
+ ```sql
194
+ -- You see this
195
+ EXPLAIN ANALYZE SELECT dzql.generic_exec('save', 'venues', '...');
196
+
197
+ -- But the actual query is hidden inside
198
+ ```
199
+
200
+ ### Compiler Mode
201
+
202
+ Standard PostgreSQL tools work:
203
+
204
+ ```sql
205
+ -- See the actual function
206
+ \sf save_venues
207
+
208
+ -- Analyze performance
209
+ EXPLAIN ANALYZE SELECT save_venues('{"name": "MSG"}'::jsonb, 42);
210
+
211
+ -- Check slow queries
212
+ SELECT * FROM pg_stat_statements WHERE query LIKE '%save_venues%';
213
+ ```
214
+
215
+ ## Feature Parity
216
+
217
+ Both modes support the same features:
218
+
219
+ | Feature | Interpreter | Compiler |
220
+ |---------|-------------|----------|
221
+ | CRUD operations | โœ… | โœ… |
222
+ | Permission paths | โœ… | โœ… |
223
+ | Graph rules | โœ… | โœ… |
224
+ | Notification paths | โœ… | โœ… |
225
+ | FK includes | โœ… | โœ… |
226
+ | Many-to-many | โœ… | โœ… |
227
+ | Field defaults | โœ… | โœ… |
228
+ | Soft delete | โœ… | โœ… |
229
+ | Temporal fields | โœ… | โœ… |
230
+
231
+ The difference is purely in execution speed and debuggability, not functionality.
232
+
233
+ ## See Also
234
+
235
+ - [Compiler Quickstart](../compiler/QUICKSTART.md) - Get started with compilation
236
+ - [Compiler Comparison](../compiler/COMPARISON.md) - Detailed side-by-side analysis
237
+ - [API Reference](../reference/api.md) - The 5 operations
@@ -161,6 +161,23 @@ SELECT dzql.register_entity(
161
161
  | `id_field` | Yes | Field name for ID array in API | `"tag_ids"` |
162
162
  | `expand` | No | Include full objects (default: false) | `false` or `true` |
163
163
 
164
+ ### Naming Convention for `id_field`
165
+
166
+ **Important:** The `id_field` should use the **singular** form of the target entity, not plural:
167
+
168
+ | Target Entity | Correct `id_field` | Wrong |
169
+ |---------------|-------------------|-------|
170
+ | `tags` | `tag_ids` | `tags_ids` |
171
+ | `roles` | `role_ids` | `roles_ids` |
172
+ | `categories` | `category_ids` | `categories_ids` |
173
+ | `users` | `user_ids` | `users_ids` |
174
+
175
+ This convention matches common ORM patterns and is more readable:
176
+ - `tag_ids: [1, 2, 3]` reads as "tag IDs"
177
+ - `tags_ids: [1, 2, 3]` reads awkwardly as "tags IDs"
178
+
179
+ Using the wrong naming will cause the M2M sync to fail because the generated code looks for a field name that doesn't match what clients send.
180
+
164
181
  ### The `expand` Flag
165
182
 
166
183
  Controls whether full related objects are included in responses:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.4.2",
3
+ "version": "0.4.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",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "test": "bun test ../../tests/core/*.test.js",
27
- "prepublishOnly": "echo 'โœ… Publishing DZQL v0.3.5...'"
27
+ "prepublishOnly": "echo 'โœ… Publishing DZQL...' "
28
28
  },
29
29
  "dependencies": {
30
30
  "jose": "^6.1.0",
@@ -39,24 +39,43 @@ export class GraphRulesCodegen {
39
39
  const operation = trigger.replace('on_', ''); // on_create -> create
40
40
  const functionName = `_graph_${this.tableName}_${trigger}`;
41
41
 
42
- const actionBlocks = [];
42
+ const ruleBlocks = [];
43
43
 
44
44
  // Process each rule
45
45
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
46
46
  const description = ruleConfig.description || ruleName;
47
+ const condition = ruleConfig.condition;
47
48
  const actions = Array.isArray(ruleConfig.actions)
48
49
  ? ruleConfig.actions
49
50
  : (ruleConfig.actions ? [ruleConfig.actions] : []);
50
51
 
52
+ const actionBlocks = [];
51
53
  for (const action of actions) {
52
54
  const actionSQL = this._generateAction(action, ruleName, description);
53
55
  if (actionSQL) {
54
56
  actionBlocks.push(actionSQL);
55
57
  }
56
58
  }
59
+
60
+ if (actionBlocks.length === 0) {
61
+ continue; // Skip rules with no actions
62
+ }
63
+
64
+ // Wrap actions in condition IF block if condition is present
65
+ if (condition) {
66
+ const conditionSQL = this._generateCondition(condition, operation);
67
+ const ruleBlock = ` -- Rule: ${ruleName}
68
+ IF ${conditionSQL} THEN
69
+ ${actionBlocks.join('\n\n')}
70
+ END IF;`;
71
+ ruleBlocks.push(ruleBlock);
72
+ } else {
73
+ // No condition - add actions directly
74
+ ruleBlocks.push(...actionBlocks);
75
+ }
57
76
  }
58
77
 
59
- if (actionBlocks.length === 0) {
78
+ if (ruleBlocks.length === 0) {
60
79
  return null; // No actions, no function
61
80
  }
62
81
 
@@ -72,7 +91,7 @@ CREATE OR REPLACE FUNCTION ${functionName}(
72
91
  ${params}
73
92
  ) RETURNS VOID AS $$
74
93
  BEGIN
75
- ${actionBlocks.join('\n\n')}
94
+ ${ruleBlocks.join('\n\n')}
76
95
  END;
77
96
  $$ LANGUAGE plpgsql SECURITY DEFINER;`;
78
97
  }
@@ -100,6 +119,9 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
100
119
  case 'execute':
101
120
  return this._generateExecuteAction(action, comment);
102
121
 
122
+ case 'notify':
123
+ return this._generateNotifyAction(action, comment);
124
+
103
125
  default:
104
126
  console.warn('Unknown action type:', action.type);
105
127
  return null;
@@ -211,6 +233,140 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
211
233
  PERFORM ${functionName}(${paramSQL});`;
212
234
  }
213
235
 
236
+ /**
237
+ * Generate NOTIFY action
238
+ * Creates an event that will be broadcast to specified users
239
+ * @private
240
+ */
241
+ _generateNotifyAction(action, comment) {
242
+ const users = action.users || [];
243
+ const message = action.message || '';
244
+ const data = action.data || {};
245
+
246
+ // Build user ID array resolution
247
+ let userIdSQL = 'ARRAY[]::INT[]';
248
+
249
+ if (users.length > 0) {
250
+ // Users can be paths like "@post_id->posts.author_id" or direct field refs like "@author_id"
251
+ const userPaths = [];
252
+
253
+ for (const userPath of users) {
254
+ if (userPath.startsWith('@') && !userPath.includes('->')) {
255
+ // Simple field reference: @author_id
256
+ const fieldName = userPath.substring(1);
257
+ userPaths.push(`(p_record->>'${fieldName}')::int`);
258
+ } else if (userPath.startsWith('@') && userPath.includes('->')) {
259
+ // Complex path: @post_id->posts.author_id - use runtime resolver
260
+ userPaths.push(`dzql.resolve_notification_path('${this.tableName}', p_record, '${userPath}')`);
261
+ } else {
262
+ // Literal user ID
263
+ userPaths.push(`${userPath}`);
264
+ }
265
+ }
266
+
267
+ if (userPaths.length === 1 && !userPaths[0].includes('resolve_notification_path')) {
268
+ // Single simple field - wrap in array
269
+ userIdSQL = `ARRAY[${userPaths[0]}]`;
270
+ } else if (userPaths.length === 1) {
271
+ // Single path resolution (already returns array)
272
+ userIdSQL = userPaths[0];
273
+ } else {
274
+ // Multiple paths - need to combine arrays
275
+ userIdSQL = `(${userPaths.map(p =>
276
+ p.includes('resolve_notification_path')
277
+ ? p
278
+ : `ARRAY[${p}]`
279
+ ).join(' || ')})`;
280
+ }
281
+ }
282
+
283
+ // Build notification data object
284
+ const dataFields = [];
285
+ dataFields.push(`'type', 'graph_rule_notification'`);
286
+ dataFields.push(`'table', '${this.tableName}'`);
287
+
288
+ if (message) {
289
+ dataFields.push(`'message', ${this._resolveValue(message)}`);
290
+ }
291
+
292
+ // Add custom data fields
293
+ for (const [key, value] of Object.entries(data)) {
294
+ dataFields.push(`'${key}', ${this._resolveValue(value)}`);
295
+ }
296
+
297
+ const dataSQL = dataFields.length > 0
298
+ ? `jsonb_build_object(${dataFields.join(', ')})`
299
+ : "'{}'::jsonb";
300
+
301
+ return `${comment}
302
+ -- Create notification event
303
+ INSERT INTO dzql.events (
304
+ table_name,
305
+ op,
306
+ pk,
307
+ data,
308
+ user_id,
309
+ notify_users
310
+ ) VALUES (
311
+ '${this.tableName}',
312
+ 'notify',
313
+ jsonb_build_object('id', (p_record->>'id')::int),
314
+ ${dataSQL},
315
+ p_user_id,
316
+ ${userIdSQL}
317
+ );`;
318
+ }
319
+
320
+ /**
321
+ * Generate condition SQL from condition string
322
+ * Supports @before.field, @after.field, @user_id, @id
323
+ * @private
324
+ */
325
+ _generateCondition(condition, operation) {
326
+ let conditionSQL = condition;
327
+
328
+ // Replace @before.field references (for update/delete)
329
+ conditionSQL = conditionSQL.replace(/@before\.(\w+)/g, (match, field) => {
330
+ return `(p_old_record->>'${field}')`;
331
+ });
332
+
333
+ // Replace @after.field references (for update)
334
+ conditionSQL = conditionSQL.replace(/@after\.(\w+)/g, (match, field) => {
335
+ if (operation === 'update') {
336
+ return `(p_new_record->>'${field}')`;
337
+ } else {
338
+ return `(p_record->>'${field}')`;
339
+ }
340
+ });
341
+
342
+ // Replace @field references (current record)
343
+ conditionSQL = conditionSQL.replace(/@(\w+)(?!\w)/g, (match, field) => {
344
+ if (field === 'user_id') {
345
+ return 'p_user_id';
346
+ } else if (field === 'id') {
347
+ // Use appropriate record based on operation
348
+ if (operation === 'update') {
349
+ return `(p_new_record->>'id')`;
350
+ } else if (operation === 'delete') {
351
+ return `(p_old_record->>'id')`;
352
+ } else {
353
+ return `(p_record->>'id')`;
354
+ }
355
+ } else {
356
+ // Field from current record
357
+ if (operation === 'update') {
358
+ return `(p_new_record->>'${field}')`;
359
+ } else if (operation === 'delete') {
360
+ return `(p_old_record->>'${field}')`;
361
+ } else {
362
+ return `(p_record->>'${field}')`;
363
+ }
364
+ }
365
+ });
366
+
367
+ return conditionSQL;
368
+ }
369
+
214
370
  /**
215
371
  * Resolve a value (variable reference or literal)
216
372
  * @private
@@ -729,10 +729,11 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
729
729
  }
730
730
 
731
731
  /**
732
- * Resolve a variable default (@user_id, @now, @today) to SQL expression
732
+ * Resolve a variable default (@user_id, @now, @today, @field_name) to SQL expression
733
733
  * @private
734
734
  */
735
735
  _resolveDefaultVariable(variable, fieldName) {
736
+ // Handle built-in variables
736
737
  switch (variable) {
737
738
  case '@user_id':
738
739
  return 'p_user_id';
@@ -740,9 +741,16 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
740
741
  return `to_char(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
741
742
  case '@today':
742
743
  return `to_char(CURRENT_DATE, 'YYYY-MM-DD')`;
743
- default:
744
- throw new Error(`Unknown field default variable: ${variable} for field ${fieldName}`);
745
744
  }
745
+
746
+ // Handle field references: @other_field
747
+ if (variable.startsWith('@')) {
748
+ const referencedField = variable.substring(1);
749
+ // Reference to another field in the data being inserted
750
+ return `(p_data->>'${referencedField}')`;
751
+ }
752
+
753
+ throw new Error(`Unknown field default variable: ${variable} for field ${fieldName}`);
746
754
  }
747
755
 
748
756
  /**
@@ -228,7 +228,11 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
228
228
 
229
229
  // Add temporal condition
230
230
  if (temporal) {
231
- conditions.push(`${targetTable}.valid_to IS NULL`);
231
+ // Add temporal filtering for {active} marker
232
+ // Assumes standard field names: valid_from and valid_to
233
+ // This matches the interpreter's behavior in resolve_path_segment (002_functions.sql:316)
234
+ conditions.push(`${targetTable}.valid_from <= NOW()`);
235
+ conditions.push(`(${targetTable}.valid_to > NOW() OR ${targetTable}.valid_to IS NULL)`);
232
236
  }
233
237
 
234
238
  // Add user_id check (final target)
package/src/server/db.js CHANGED
@@ -224,8 +224,13 @@ export async function callDZQLOperation(operation, entity, args, userId) {
224
224
  throw new Error(`Unknown operation: ${operation}`);
225
225
  }
226
226
  } catch (error) {
227
- // If compiled function doesn't exist, fall back to generic_exec
228
- if (error.message?.includes('does not exist') || error.code === '42883') {
227
+ // Only fall back if the COMPILED function itself doesn't exist
228
+ // Don't fall back for other "does not exist" errors (e.g., missing tables, downstream functions)
229
+ const isMissingCompiledFunction =
230
+ (error.message?.includes('does not exist') || error.code === '42883') &&
231
+ error.message?.includes(compiledFunctionName);
232
+
233
+ if (isMissingCompiledFunction) {
229
234
  dbLogger.trace(`Compiled function ${compiledFunctionName} not found, trying generic_exec`);
230
235
  const result = await sql`
231
236
  SELECT dzql.generic_exec(${operation}, ${entity}, ${args}, ${userId}) as result
@@ -31,12 +31,12 @@ async function processSubscriptionUpdates(event, broadcast) {
31
31
  for (const [subscribableName, subs] of subscriptionsByName.entries()) {
32
32
  try {
33
33
  // Ask PostgreSQL which subscription instances are affected
34
- const result = await db.query(
34
+ const result = await sql.unsafe(
35
35
  `SELECT ${subscribableName}_affected_documents($1, $2, $3, $4) as affected`,
36
36
  [table, op, before, after]
37
37
  );
38
38
 
39
- const affectedParamSets = result.rows[0]?.affected;
39
+ const affectedParamSets = result[0]?.affected;
40
40
 
41
41
  if (!affectedParamSets || affectedParamSets.length === 0) {
42
42
  continue; // This subscribable not affected
@@ -51,12 +51,12 @@ async function processSubscriptionUpdates(event, broadcast) {
51
51
  if (paramsMatch(sub.params, affectedParams)) {
52
52
  try {
53
53
  // Re-execute query to get updated data
54
- const updated = await db.query(
54
+ const updated = await sql.unsafe(
55
55
  `SELECT get_${subscribableName}($1, $2) as data`,
56
56
  [sub.params, sub.user_id]
57
57
  );
58
58
 
59
- const data = updated.rows[0]?.data;
59
+ const data = updated[0]?.data;
60
60
 
61
61
  // Send update to specific connection
62
62
  const message = JSON.stringify({
package/src/server/ws.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  callUserFunction,
5
5
  getUserProfile,
6
6
  db,
7
+ sql,
7
8
  } from "./db.js";
8
9
  import { wsLogger, authLogger } from "./logger.js";
9
10
  import {
@@ -317,12 +318,12 @@ export function createRPCHandler(customHandlers = {}) {
317
318
 
318
319
  try {
319
320
  // Execute initial query (this also checks permissions)
320
- const queryResult = await db.query(
321
+ const queryResult = await sql.unsafe(
321
322
  `SELECT get_${subscribableName}($1, $2) as data`,
322
323
  [params, ws.data.user_id]
323
324
  );
324
325
 
325
- const data = queryResult.rows[0]?.data;
326
+ const data = queryResult[0]?.data;
326
327
 
327
328
  // Register subscription in memory
328
329
  const subscriptionId = registerSubscription(
@@ -1,95 +0,0 @@
1
- # DZQL Canonical Pinia Stores
2
-
3
- **The official, AI-friendly Pinia stores for DZQL Vue.js applications.**
4
-
5
- ## Why These Stores Exist
6
-
7
- When building DZQL apps, developers (and AI assistants) often struggle with:
8
-
9
- 1. **Three-phase lifecycle** - connecting โ†’ login โ†’ ready
10
- 2. **WebSocket connection management** - reconnection, error handling
11
- 3. **Authentication flow** - token storage, profile management
12
- 4. **Router integration** - navigation, state synchronization
13
- 5. **Inconsistent patterns** - every project does it differently
14
-
15
- These canonical stores solve all of these problems with a **simple, consistent pattern** that AI can easily understand and replicate.
16
-
17
- ## The Stores
18
-
19
- ### `useWsStore` - WebSocket & Auth
20
-
21
- Manages:
22
- - WebSocket connection (with auto-reconnect)
23
- - User authentication (login/register/logout)
24
- - Connection state tracking
25
- - Three-phase app lifecycle
26
-
27
- ### `useAppStore` - Application State
28
-
29
- Manages:
30
- - App initialization
31
- - Router integration
32
- - Entity metadata caching
33
- - Navigation helpers
34
- - UI state (sidebars, panels)
35
-
36
- ## Quick Example
37
-
38
- ```vue
39
- <script setup>
40
- import { computed } from 'vue'
41
- import { useWsStore, useAppStore } from 'dzql/client/stores'
42
-
43
- const wsStore = useWsStore()
44
- const appStore = useAppStore()
45
-
46
- const state = computed(() => wsStore.appState)
47
- const ws = wsStore.getWs()
48
-
49
- // Use DZQL API
50
- const venues = await ws.api.search.venues({ limit: 50 })
51
- </script>
52
-
53
- <template>
54
- <!-- Three-phase lifecycle -->
55
- <div v-if="state === 'connecting'">Connecting...</div>
56
- <LoginView v-else-if="state === 'login'" />
57
- <MainApp v-else-if="state === 'ready'" />
58
- </template>
59
- ```
60
-
61
- ## Documentation
62
-
63
- - **[Quick Start](../../../docs/reference/client.md)** - Get running in 5 minutes
64
- - **[Complete Guide](../../../docs/guides/client-stores.md)** - Full API reference and patterns
65
-
66
- ## Files
67
-
68
- - `useWsStore.js` - WebSocket connection and authentication
69
- - `useAppStore.js` - Application state and navigation
70
- - `index.js` - Exports both stores
71
-
72
- ## For AI Assistants
73
-
74
- When helping users with DZQL apps:
75
-
76
- 1. **Always use these stores** - Don't create custom connection logic
77
- 2. **Follow the three-phase lifecycle** - connecting โ†’ login โ†’ ready
78
- 3. **Use computed for reactive state** - `const profile = computed(() => wsStore.profile)`
79
- 4. **Get WS instance for API calls** - `const ws = wsStore.getWs()`
80
-
81
- **Example prompt for AI:**
82
-
83
- > "I'm using the canonical DZQL stores from `dzql/client/stores`. The pattern is:
84
- > 1. useWsStore for WebSocket connection (three phases: connecting, login, ready)
85
- > 2. useAppStore for app state and navigation
86
- > 3. Access DZQL API via `wsStore.getWs().api.get.venues({ id: 1 })`
87
- > Please follow this pattern."
88
-
89
- ## Version
90
-
91
- These stores are available in DZQL v0.1.6+
92
-
93
- ## License
94
-
95
- MIT