dzql 0.1.4 → 0.1.5

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,183 @@
1
+ # Advanced SEARCH Filters
2
+
3
+ The compiled SEARCH functions support powerful JSONB-based filtering with multiple operators.
4
+
5
+ ## Filter Operators
6
+
7
+ ### Comparison Operators
8
+
9
+ **Equal (`eq`)** - Exact match
10
+ ```json
11
+ { "status": { "eq": "active" } }
12
+ ```
13
+ or simplified:
14
+ ```json
15
+ { "status": "active" }
16
+ ```
17
+
18
+ **Not Equal (`ne`)** - Exclude values
19
+ ```json
20
+ { "status": { "ne": "deleted" } }
21
+ ```
22
+
23
+ **Greater Than (`gt`)**
24
+ ```json
25
+ { "age": { "gt": 18 } }
26
+ ```
27
+
28
+ **Greater Than or Equal (`gte`)**
29
+ ```json
30
+ { "age": { "gte": 18 } }
31
+ ```
32
+
33
+ **Less Than (`lt`)**
34
+ ```json
35
+ { "created_at": { "lt": "2024-01-01" } }
36
+ ```
37
+
38
+ **Less Than or Equal (`lte`)**
39
+ ```json
40
+ { "price": { "lte": 100 } }
41
+ ```
42
+
43
+ ### Array Membership
44
+
45
+ **In (`in`)** - Match any value in array
46
+ ```json
47
+ { "status": { "in": ["active", "pending", "review"] } }
48
+ ```
49
+
50
+ ### Pattern Matching
51
+
52
+ **Case-Insensitive Like (`ilike`)** - PostgreSQL pattern matching
53
+ ```json
54
+ { "email": { "ilike": "%@gmail.com" } }
55
+ ```
56
+
57
+ **Like (`like`)** - Case-sensitive pattern matching
58
+ ```json
59
+ { "code": { "like": "PRD-%" } }
60
+ ```
61
+
62
+ ## Combining Multiple Filters
63
+
64
+ You can combine multiple filters on different fields:
65
+
66
+ ```json
67
+ {
68
+ "age": { "gte": 18, "lte": 65 },
69
+ "status": { "in": ["active", "pending"] },
70
+ "email": { "ilike": "%@company.com" }
71
+ }
72
+ ```
73
+
74
+ You can also apply multiple operators to the same field:
75
+
76
+ ```json
77
+ {
78
+ "age": { "gte": 18, "lt": 65 }
79
+ }
80
+ ```
81
+
82
+ ## Example Usage
83
+
84
+ ### Simple equality filter
85
+ ```javascript
86
+ const result = await sql`
87
+ SELECT search_users(
88
+ p_filters := '{"status": "active"}'::jsonb,
89
+ p_page := 1,
90
+ p_limit := 25
91
+ )
92
+ `;
93
+ ```
94
+
95
+ ### Age range filter
96
+ ```javascript
97
+ const result = await sql`
98
+ SELECT search_users(
99
+ p_filters := '{"age": {"gte": 18, "lte": 65}}'::jsonb
100
+ )
101
+ `;
102
+ ```
103
+
104
+ ### Multiple status filter
105
+ ```javascript
106
+ const result = await sql`
107
+ SELECT search_orders(
108
+ p_filters := '{"status": {"in": ["pending", "processing", "shipped"]}}'::jsonb
109
+ )
110
+ `;
111
+ ```
112
+
113
+ ### Email domain filter with text search
114
+ ```javascript
115
+ const result = await sql`
116
+ SELECT search_users(
117
+ p_filters := '{"email": {"ilike": "%@company.com"}}'::jsonb,
118
+ p_search := 'john',
119
+ p_sort := '{"field": "created_at", "order": "desc"}'::jsonb
120
+ )
121
+ `;
122
+ ```
123
+
124
+ ### Complex combined filters
125
+ ```javascript
126
+ const result = await sql`
127
+ SELECT search_products(
128
+ p_filters := '{
129
+ "price": {"gte": 10, "lte": 100},
130
+ "category": {"in": ["electronics", "accessories"]},
131
+ "status": "active",
132
+ "name": {"ilike": "%wireless%"}
133
+ }'::jsonb,
134
+ p_page := 1,
135
+ p_limit := 50
136
+ )
137
+ `;
138
+ ```
139
+
140
+ ## Generated SQL
141
+
142
+ The filters are converted to optimized SQL WHERE conditions:
143
+
144
+ ```sql
145
+ -- Input filters:
146
+ { "age": {"gte": 18}, "status": {"in": ["active", "pending"]} }
147
+
148
+ -- Generated WHERE clause:
149
+ WHERE TRUE
150
+ AND age >= '18'
151
+ AND status = ANY(ARRAY['active', 'pending']::TEXT[])
152
+ ```
153
+
154
+ ## Performance Considerations
155
+
156
+ 1. **Index your filter fields** - Create indexes on frequently filtered columns:
157
+ ```sql
158
+ CREATE INDEX idx_users_status ON users(status);
159
+ CREATE INDEX idx_users_age ON users(age);
160
+ ```
161
+
162
+ 2. **Use exact matches when possible** - `eq` is faster than `ilike`
163
+
164
+ 3. **Limit ILIKE patterns** - Patterns starting with `%` can't use indexes efficiently
165
+
166
+ 4. **Combine with pagination** - Always use `p_limit` to avoid loading large result sets
167
+
168
+ ## Type Safety
169
+
170
+ The filter system is type-aware and handles:
171
+ - **Strings** - Quoted and escaped properly
172
+ - **Numbers** - Cast appropriately
173
+ - **Booleans** - Converted to SQL boolean values
174
+ - **Dates** - Handled as timestamp comparisons
175
+ - **Arrays** - Expanded for `IN` clauses
176
+
177
+ ## Error Handling
178
+
179
+ Unknown operators are silently ignored to prevent SQL injection. Only these operators are supported:
180
+ - `eq`, `ne`
181
+ - `gt`, `gte`, `lt`, `lte`
182
+ - `in`
183
+ - `like`, `ilike`
@@ -0,0 +1,349 @@
1
+ # DZQL Compiler - Coding Standards
2
+
3
+ This document defines the coding standards that the DZQL Compiler enforces when generating PostgreSQL functions.
4
+
5
+ ## Function Naming Conventions
6
+
7
+ ### 1. Parameter Naming
8
+
9
+ **All parameters MUST use the `p_` prefix:**
10
+
11
+ ```sql
12
+ -- ✅ CORRECT
13
+ CREATE FUNCTION get_users(
14
+ p_user_id INT,
15
+ p_id INT,
16
+ p_on_date TIMESTAMPTZ DEFAULT NULL
17
+ )
18
+
19
+ -- ❌ WRONG
20
+ CREATE FUNCTION get_users(
21
+ user_id INT,
22
+ id INT,
23
+ on_date TIMESTAMPTZ DEFAULT NULL
24
+ )
25
+ ```
26
+
27
+ ### 2. Parameter Ordering
28
+
29
+ **`p_user_id INT` MUST be the first parameter in ALL functions:**
30
+
31
+ This is a critical security requirement from the DZQL framework - the authenticated user ID must always be the first parameter.
32
+
33
+ ```sql
34
+ -- ✅ CORRECT - p_user_id first
35
+ CREATE FUNCTION get_users(p_user_id INT, p_id INT, ...)
36
+ CREATE FUNCTION save_users(p_user_id INT, p_data JSONB)
37
+ CREATE FUNCTION delete_users(p_user_id INT, p_id INT)
38
+ CREATE FUNCTION lookup_users(p_user_id INT, p_filter TEXT, ...)
39
+ CREATE FUNCTION search_users(p_user_id INT, p_filters JSONB, ...)
40
+
41
+ -- ❌ WRONG - p_user_id not first
42
+ CREATE FUNCTION get_users(p_id INT, p_user_id INT, ...)
43
+ CREATE FUNCTION save_users(p_data JSONB, p_user_id INT)
44
+ ```
45
+
46
+ ### 3. Helper Function Prefixes
47
+
48
+ **Helper functions MUST start with underscore `_` to prevent direct websocket access:**
49
+
50
+ Helper functions are internal implementation details that should not be callable by websocket clients.
51
+
52
+ ```sql
53
+ -- ✅ CORRECT - Helper functions with underscore
54
+ CREATE FUNCTION _graph_users_on_create(p_user_id INT, p_record JSONB)
55
+ CREATE FUNCTION _resolve_notification_paths_users(p_user_id INT, p_record JSONB)
56
+
57
+ -- ❌ WRONG - Helper functions without underscore (publicly callable!)
58
+ CREATE FUNCTION graph_users_on_create(p_record JSONB, p_user_id INT)
59
+ CREATE FUNCTION resolve_notification_paths_users(p_record JSONB)
60
+ ```
61
+
62
+ **Public API functions do NOT use underscore prefix:**
63
+
64
+ ```sql
65
+ -- ✅ CORRECT - Public API functions
66
+ CREATE FUNCTION can_view_users(p_user_id INT, p_record JSONB)
67
+ CREATE FUNCTION can_create_users(p_user_id INT, p_record JSONB)
68
+ CREATE FUNCTION get_users(p_user_id INT, p_id INT, ...)
69
+ CREATE FUNCTION save_users(p_user_id INT, p_data JSONB)
70
+ CREATE FUNCTION delete_users(p_user_id INT, p_id INT)
71
+ CREATE FUNCTION lookup_users(p_user_id INT, p_filter TEXT, ...)
72
+ CREATE FUNCTION search_users(p_user_id INT, p_filters JSONB, ...)
73
+ ```
74
+
75
+ ## Function Categories
76
+
77
+ ### Public API Functions (No underscore)
78
+
79
+ These are directly callable via the DZQL websocket API:
80
+
81
+ 1. **Permission Check Functions** - `can_{operation}_{table}`
82
+ - `can_view_{table}(p_user_id INT, p_record JSONB)`
83
+ - `can_create_{table}(p_user_id INT, p_record JSONB)`
84
+ - `can_update_{table}(p_user_id INT, p_record JSONB)`
85
+ - `can_delete_{table}(p_user_id INT, p_record JSONB)`
86
+
87
+ 2. **CRUD Operation Functions**
88
+ - `get_{table}(p_user_id INT, p_id INT, p_on_date TIMESTAMPTZ DEFAULT NULL)`
89
+ - `save_{table}(p_user_id INT, p_data JSONB)`
90
+ - `delete_{table}(p_user_id INT, p_id INT)`
91
+ - `lookup_{table}(p_user_id INT, p_filter TEXT DEFAULT NULL, p_limit INT DEFAULT 50)`
92
+ - `search_{table}(p_user_id INT, p_filters JSONB DEFAULT '{}', ...)`
93
+
94
+ ### Helper Functions (With underscore)
95
+
96
+ These are internal and NOT directly callable via websocket:
97
+
98
+ 1. **Graph Rule Functions** - `_graph_{table}_{trigger}`
99
+ - `_graph_{table}_on_create(p_user_id INT, p_record JSONB)`
100
+ - `_graph_{table}_on_update(p_user_id INT, p_old_record JSONB, p_new_record JSONB)`
101
+ - `_graph_{table}_on_delete(p_user_id INT, p_old_record JSONB)`
102
+
103
+ 2. **Notification Resolution Functions** - `_resolve_notification_paths_{table}`
104
+ - `_resolve_notification_paths_{table}(p_user_id INT, p_record JSONB)`
105
+
106
+ ## Standard Permission Functions
107
+
108
+ **ALL entities MUST have all 4 permission check functions generated:**
109
+
110
+ Even if an entity has no permission restrictions (public access), all 4 functions must exist:
111
+
112
+ ```sql
113
+ -- Public access - returns true
114
+ CREATE FUNCTION can_view_users(p_user_id INT, p_record JSONB)
115
+ RETURNS BOOLEAN AS $$
116
+ BEGIN
117
+ RETURN true; -- Public access
118
+ END;
119
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
120
+
121
+ -- Restricted access - checks permissions
122
+ CREATE FUNCTION can_update_users(p_user_id INT, p_record JSONB)
123
+ RETURNS BOOLEAN AS $$
124
+ BEGIN
125
+ RETURN EXISTS (
126
+ SELECT 1 FROM acts_for
127
+ WHERE acts_for.org_id = (p_record->>'org_id')::int
128
+ AND acts_for.user_id = p_user_id
129
+ AND acts_for.valid_to IS NULL
130
+ );
131
+ END;
132
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
133
+ ```
134
+
135
+ ## Variable Naming
136
+
137
+ ### Local Variables
138
+
139
+ **Use `v_` prefix for all local variables:**
140
+
141
+ ```sql
142
+ DECLARE
143
+ v_result users%ROWTYPE; -- ✅ CORRECT
144
+ v_existing users%ROWTYPE; -- ✅ CORRECT
145
+ v_is_insert BOOLEAN := false; -- ✅ CORRECT
146
+ v_notify_users INT[]; -- ✅ CORRECT
147
+ BEGIN
148
+ -- ...
149
+ END;
150
+ ```
151
+
152
+ ## SQL Style
153
+
154
+ ### Keywords
155
+
156
+ **All SQL keywords should be UPPERCASE:**
157
+
158
+ ```sql
159
+ -- ✅ CORRECT
160
+ CREATE OR REPLACE FUNCTION get_users(...)
161
+ RETURNS JSONB AS $$
162
+ DECLARE
163
+ v_result JSONB;
164
+ BEGIN
165
+ SELECT * INTO v_result FROM users WHERE id = p_id;
166
+ RETURN v_result;
167
+ END;
168
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
169
+
170
+ -- ❌ WRONG
171
+ create or replace function get_users(...)
172
+ returns jsonb as $$
173
+ declare
174
+ v_result jsonb;
175
+ begin
176
+ select * into v_result from users where id = p_id;
177
+ return v_result;
178
+ end;
179
+ $$ language plpgsql security definer;
180
+ ```
181
+
182
+ ### Function Attributes
183
+
184
+ **All functions MUST have:**
185
+ - `LANGUAGE plpgsql` (or `LANGUAGE sql`)
186
+ - `SECURITY DEFINER` - Runs with privileges of the function owner
187
+
188
+ **Permission functions should additionally have:**
189
+ - `STABLE` - Indicates function doesn't modify the database
190
+
191
+ ```sql
192
+ -- Permission function
193
+ CREATE FUNCTION can_view_users(...)
194
+ RETURNS BOOLEAN AS $$
195
+ -- ...
196
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
197
+
198
+ -- Operation function
199
+ CREATE FUNCTION save_users(...)
200
+ RETURNS JSONB AS $$
201
+ -- ...
202
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
203
+ ```
204
+
205
+ ## Complete Example
206
+
207
+ Here's a complete example showing all coding standards:
208
+
209
+ ```sql
210
+ -- ============================================================================
211
+ -- Permission Functions (Public API - no underscore)
212
+ -- ============================================================================
213
+
214
+ -- Permission check: view on organisations
215
+ CREATE OR REPLACE FUNCTION can_view_organisations(
216
+ p_user_id INT,
217
+ p_record JSONB
218
+ ) RETURNS BOOLEAN AS $$
219
+ BEGIN
220
+ RETURN true; -- Public access
221
+ END;
222
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
223
+
224
+ -- ============================================================================
225
+ -- CRUD Operations (Public API - no underscore)
226
+ -- ============================================================================
227
+
228
+ -- GET operation for organisations
229
+ CREATE OR REPLACE FUNCTION get_organisations(
230
+ p_user_id INT,
231
+ p_id INT,
232
+ p_on_date TIMESTAMPTZ DEFAULT NULL
233
+ ) RETURNS JSONB AS $$
234
+ DECLARE
235
+ v_result JSONB;
236
+ v_record organisations%ROWTYPE;
237
+ BEGIN
238
+ -- Fetch the record
239
+ SELECT * INTO v_record FROM organisations WHERE id = p_id;
240
+
241
+ IF NOT FOUND THEN
242
+ RAISE EXCEPTION 'Record not found: % with id=%', 'organisations', p_id;
243
+ END IF;
244
+
245
+ -- Convert to JSONB
246
+ v_result := to_jsonb(v_record);
247
+
248
+ -- Check view permission
249
+ IF NOT can_view_organisations(p_user_id, v_result) THEN
250
+ RAISE EXCEPTION 'Permission denied: view on organisations';
251
+ END IF;
252
+
253
+ RETURN v_result;
254
+ END;
255
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
256
+
257
+ -- SAVE operation for organisations
258
+ CREATE OR REPLACE FUNCTION save_organisations(
259
+ p_user_id INT,
260
+ p_data JSONB
261
+ ) RETURNS JSONB AS $$
262
+ DECLARE
263
+ v_result organisations%ROWTYPE;
264
+ v_is_insert BOOLEAN;
265
+ BEGIN
266
+ -- Perform UPSERT
267
+ -- ... implementation ...
268
+
269
+ -- Call graph rules helper
270
+ IF v_is_insert THEN
271
+ PERFORM _graph_organisations_on_create(p_user_id, to_jsonb(v_result));
272
+ END IF;
273
+
274
+ RETURN to_jsonb(v_result);
275
+ END;
276
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
277
+
278
+ -- ============================================================================
279
+ -- Helper Functions (Internal - with underscore)
280
+ -- ============================================================================
281
+
282
+ -- Graph rules: on_create on organisations
283
+ CREATE OR REPLACE FUNCTION _graph_organisations_on_create(
284
+ p_user_id INT,
285
+ p_record JSONB
286
+ ) RETURNS VOID AS $$
287
+ BEGIN
288
+ -- Creator becomes owner
289
+ INSERT INTO acts_for (user_id, org_id, valid_from)
290
+ VALUES (p_user_id, (p_record->>'id'), CURRENT_DATE);
291
+ END;
292
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
293
+
294
+ -- Notification path resolution for organisations
295
+ CREATE OR REPLACE FUNCTION _resolve_notification_paths_organisations(
296
+ p_user_id INT,
297
+ p_record JSONB
298
+ ) RETURNS INT[] AS $$
299
+ DECLARE
300
+ v_users INT[] := ARRAY[]::INT[];
301
+ BEGIN
302
+ -- Resolve notification recipients
303
+ -- ... implementation ...
304
+
305
+ RETURN ARRAY(SELECT DISTINCT unnest(v_users));
306
+ END;
307
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
308
+ ```
309
+
310
+ ## Why These Standards Matter
311
+
312
+ ### Security
313
+
314
+ 1. **`p_user_id` first** - Ensures user context is always explicit and hard to forget
315
+ 2. **Helper functions with `_`** - Prevents direct websocket access to internal functions
316
+ 3. **SECURITY DEFINER** - Ensures proper privilege execution
317
+
318
+ ### Consistency
319
+
320
+ 1. **Predictable parameter order** - Makes all functions follow the same pattern
321
+ 2. **Standard naming** - Makes generated code easy to read and maintain
322
+ 3. **All 4 permission functions** - Ensures complete access control coverage
323
+
324
+ ### WebSocket Safety
325
+
326
+ The underscore prefix prevents clients from directly calling helper functions:
327
+
328
+ ```javascript
329
+ // ✅ These work - public API
330
+ await ws.api.get.users({id: 1});
331
+ await ws.api.save.users({name: 'John'});
332
+
333
+ // ❌ These DON'T work - helper functions blocked
334
+ await ws.api._graph_users_on_create({...}); // Blocked!
335
+ await ws.api._resolve_notification_paths_users({...}); // Blocked!
336
+ ```
337
+
338
+ ## Validation
339
+
340
+ The DZQL Compiler enforces these standards automatically. All generated code is validated by comprehensive tests that verify:
341
+
342
+ - ✅ Parameter naming and ordering
343
+ - ✅ Function prefixes (helper vs public)
344
+ - ✅ All 4 permission functions exist
345
+ - ✅ SQL syntax correctness
346
+ - ✅ SECURITY DEFINER attribute presence
347
+ - ✅ Uppercase SQL keywords
348
+
349
+ See `tests/sql-validation.test.js` for complete validation suite.