dzql 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,362 @@
1
+ # Custom Functions
2
+
3
+ Add custom business logic to your entities with automatic compilation support.
4
+
5
+ ## Overview
6
+
7
+ DZQL's compiler now automatically includes custom SQL functions defined after entity registration. This eliminates the need to manually maintain functions in multiple locations.
8
+
9
+ ## Benefits
10
+
11
+ - **Single Source of Truth** - Define functions once in entity files
12
+ - **No Manual Syncing** - Compiler handles everything
13
+ - **Functions Stay With Entities** - Clear organization
14
+ - **Automatic Registration** - Registry entries included
15
+
16
+ ## How It Works
17
+
18
+ ### Before (Manual Duplication)
19
+
20
+ You had to maintain functions in two places:
21
+
22
+ ```sql
23
+ -- entities/calendar.sql (source)
24
+ SELECT dzql.register_entity('tags', ...);
25
+
26
+ CREATE FUNCTION toggle_resource_tag(...) RETURNS JSONB AS $$ ... $$;
27
+ INSERT INTO dzql.registry (fn_regproc) VALUES ('toggle_resource_tag'::regproc);
28
+ ```
29
+
30
+ Then manually copy to:
31
+
32
+ ```sql
33
+ -- init_db/002_schema.sql (deployment)
34
+ CREATE FUNCTION toggle_resource_tag(...) RETURNS JSONB AS $$ ... $$;
35
+ INSERT INTO dzql.registry (fn_regproc) VALUES ('toggle_resource_tag'::regproc);
36
+ ```
37
+
38
+ **Problems:**
39
+ - Functions exist in two places
40
+ - Easy to forget to sync
41
+ - Unclear which is source of truth
42
+
43
+ ### After (Automatic Pass-through)
44
+
45
+ Just define functions once after entity registration:
46
+
47
+ ```sql
48
+ -- entities/calendar.sql
49
+ SELECT dzql.register_entity('tags', 'name', ...);
50
+
51
+ -- Custom function - automatically passed through by compiler!
52
+ CREATE OR REPLACE FUNCTION toggle_resource_tag(
53
+ p_user_id INT,
54
+ p_resource_id INT,
55
+ p_tag_id INT
56
+ ) RETURNS JSONB AS $$
57
+ DECLARE
58
+ v_exists BOOLEAN;
59
+ BEGIN
60
+ SELECT EXISTS(
61
+ SELECT 1 FROM resource_tags
62
+ WHERE resource_id = p_resource_id AND tag_id = p_tag_id
63
+ ) INTO v_exists;
64
+
65
+ IF v_exists THEN
66
+ DELETE FROM resource_tags
67
+ WHERE resource_id = p_resource_id AND tag_id = p_tag_id;
68
+ ELSE
69
+ INSERT INTO resource_tags (resource_id, tag_id)
70
+ VALUES (p_resource_id, p_tag_id);
71
+ END IF;
72
+
73
+ RETURN jsonb_build_object('success', true, 'exists', NOT v_exists);
74
+ END;
75
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
76
+
77
+ -- Register for RPC access
78
+ INSERT INTO dzql.registry (fn_regproc)
79
+ VALUES ('toggle_resource_tag'::regproc);
80
+ ```
81
+
82
+ After `dzql compile`, the custom function automatically appears in:
83
+ - `init_db/tags.sql` under "Custom Functions" section
84
+
85
+ ## What Gets Passed Through
86
+
87
+ The compiler extracts:
88
+
89
+ 1. **CREATE FUNCTION statements**
90
+ ```sql
91
+ CREATE FUNCTION my_function(...) RETURNS ... AS $$ ... $$;
92
+ CREATE OR REPLACE FUNCTION my_function(...) RETURNS ... AS $$ ... $$;
93
+ ```
94
+
95
+ 2. **Registry registrations**
96
+ ```sql
97
+ INSERT INTO dzql.registry (fn_regproc) VALUES ('my_function'::regproc);
98
+ ```
99
+
100
+ 3. **Alternative registration syntax** (if you use it)
101
+ ```sql
102
+ SELECT dzql.register_function('my_function');
103
+ ```
104
+
105
+ ## Scope
106
+
107
+ Custom functions are extracted from **after** the entity registration until:
108
+ - The next `dzql.register_entity()` call, OR
109
+ - End of file
110
+
111
+ ```sql
112
+ -- Entity 1
113
+ SELECT dzql.register_entity('users', ...);
114
+ CREATE FUNCTION user_helper() ...; -- Included with users entity
115
+
116
+ -- Entity 2
117
+ SELECT dzql.register_entity('posts', ...);
118
+ CREATE FUNCTION post_helper() ...; -- Included with posts entity
119
+ ```
120
+
121
+ Each entity gets its own custom functions isolated in the compiled output.
122
+
123
+ ## Compiled Output Format
124
+
125
+ ```sql
126
+ -- ============================================================================
127
+ -- DZQL Compiled Functions for: tags
128
+ -- Generated: 2025-11-20T15:00:00.000Z
129
+ -- ============================================================================
130
+
131
+ -- [Generated CRUD functions: get_tags, save_tags, delete_tags, etc.]
132
+
133
+ -- ============================================================================
134
+ -- Custom Functions for: tags
135
+ -- Pass-through from entity definition
136
+ -- ============================================================================
137
+
138
+ CREATE OR REPLACE FUNCTION toggle_resource_tag(...) ...;
139
+
140
+ INSERT INTO dzql.registry (fn_regproc) VALUES ('toggle_resource_tag'::regproc);
141
+ ```
142
+
143
+ ## Use Cases
144
+
145
+ ### Toggle Relationships
146
+
147
+ ```sql
148
+ CREATE OR REPLACE FUNCTION toggle_favorite(
149
+ p_user_id INT,
150
+ p_item_id INT
151
+ ) RETURNS JSONB AS $$
152
+ -- Toggle logic here
153
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
154
+ ```
155
+
156
+ ### Computed Fields
157
+
158
+ ```sql
159
+ CREATE OR REPLACE FUNCTION calculate_item_score(
160
+ p_user_id INT,
161
+ p_item_id INT
162
+ ) RETURNS JSONB AS $$
163
+ -- Scoring logic here
164
+ $$ LANGUAGE plpgsql;
165
+ ```
166
+
167
+ ### Bulk Operations
168
+
169
+ ```sql
170
+ CREATE OR REPLACE FUNCTION bulk_assign_tags(
171
+ p_user_id INT,
172
+ p_resource_ids INT[],
173
+ p_tag_id INT
174
+ ) RETURNS JSONB AS $$
175
+ -- Bulk assignment here
176
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
177
+ ```
178
+
179
+ ### Complex Business Logic
180
+
181
+ ```sql
182
+ CREATE OR REPLACE FUNCTION approve_workflow(
183
+ p_user_id INT,
184
+ p_document_id INT
185
+ ) RETURNS JSONB AS $$
186
+ -- Multi-step approval logic
187
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
188
+ ```
189
+
190
+ ## Best Practices
191
+
192
+ ### 1. Keep Functions Close to Entities
193
+
194
+ Define functions right after the related entity registration:
195
+
196
+ ```sql
197
+ SELECT dzql.register_entity('documents', ...);
198
+
199
+ -- Related custom functions
200
+ CREATE FUNCTION approve_document(...) ...;
201
+ CREATE FUNCTION reject_document(...) ...;
202
+ ```
203
+
204
+ ### 2. Always Register Functions
205
+
206
+ Don't forget to register for RPC access:
207
+
208
+ ```sql
209
+ CREATE FUNCTION my_function(...) ...;
210
+
211
+ -- Required for client access!
212
+ INSERT INTO dzql.registry (fn_regproc) VALUES ('my_function'::regproc);
213
+ ```
214
+
215
+ ### 3. Use SECURITY DEFINER Carefully
216
+
217
+ Only use `SECURITY DEFINER` when the function needs elevated privileges:
218
+
219
+ ```sql
220
+ -- Needs elevated privileges (good use of SECURITY DEFINER)
221
+ CREATE FUNCTION admin_delete_user(...)
222
+ RETURNS void AS $$ ... $$
223
+ LANGUAGE plpgsql SECURITY DEFINER;
224
+
225
+ -- User operates on own data (use SECURITY INVOKER or default)
226
+ CREATE FUNCTION update_profile(...)
227
+ RETURNS jsonb AS $$ ... $$
228
+ LANGUAGE plpgsql; -- SECURITY INVOKER is default
229
+ ```
230
+
231
+ ### 4. Include Permission Checks
232
+
233
+ Custom functions should validate permissions:
234
+
235
+ ```sql
236
+ CREATE OR REPLACE FUNCTION custom_operation(
237
+ p_user_id INT,
238
+ p_item_id INT
239
+ ) RETURNS JSONB AS $$
240
+ BEGIN
241
+ -- Always check permission first!
242
+ IF NOT dzql.check_permission(p_user_id, 'update', 'items',
243
+ (SELECT to_jsonb(items.*) FROM items WHERE id = p_item_id)
244
+ ) THEN
245
+ RAISE EXCEPTION 'Permission denied';
246
+ END IF;
247
+
248
+ -- Business logic here
249
+ ...
250
+ END;
251
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
252
+ ```
253
+
254
+ ## Limitations
255
+
256
+ ### Functions Not Related to Entities
257
+
258
+ If you have utility functions not tied to a specific entity, you have two options:
259
+
260
+ **Option 1:** Create a dedicated entity file
261
+ ```sql
262
+ -- entities/utils.sql
263
+ -- No entity registration, just functions
264
+
265
+ CREATE FUNCTION general_utility(...) ...;
266
+ INSERT INTO dzql.registry (fn_regproc) VALUES ('general_utility'::regproc);
267
+ ```
268
+
269
+ **Option 2:** Keep in manual migration file
270
+ ```sql
271
+ -- init_db/002_utilities.sql
272
+ CREATE FUNCTION general_utility(...) ...;
273
+ ```
274
+
275
+ ### Detection Logic
276
+
277
+ The compiler extracts SQL between entity registrations. Only these patterns are recognized:
278
+
279
+ - `CREATE FUNCTION ...` or `CREATE OR REPLACE FUNCTION ...`
280
+ - `INSERT INTO dzql.registry ...`
281
+ - `SELECT dzql.register_function(...)`
282
+
283
+ Other SQL (like `CREATE TYPE`, `CREATE INDEX`) is **not** automatically passed through.
284
+
285
+ ## Migration
286
+
287
+ ### For Existing Projects
288
+
289
+ If you already have custom functions duplicated:
290
+
291
+ 1. **Keep functions in entity files** (e.g., `entities/calendar.sql`)
292
+ 2. **Remove from manual migration files** (e.g., `init_db/002_schema.sql`)
293
+ 3. **Recompile:** `dzql compile entities/*.sql`
294
+ 4. **Deploy:** Updated compiled functions
295
+
296
+ Your entity files become the single source of truth!
297
+
298
+ ## Example: Complete Entity with Custom Functions
299
+
300
+ ```sql
301
+ -- entities/resources.sql
302
+
303
+ -- Create table
304
+ CREATE TABLE resources (
305
+ id serial PRIMARY KEY,
306
+ org_id integer REFERENCES organisations(id),
307
+ title text NOT NULL,
308
+ description text,
309
+ owner_id integer REFERENCES users(id)
310
+ );
311
+
312
+ -- Register entity
313
+ SELECT dzql.register_entity(
314
+ 'resources',
315
+ 'title',
316
+ ARRAY['title', 'description'],
317
+ '{"org": "organisations"}',
318
+ false,
319
+ '{}',
320
+ '{}',
321
+ '{"view": [], "create": [], "update": ["@owner_id"], "delete": ["@owner_id"]}',
322
+ '{}',
323
+ '{"owner_id": "@user_id"}'
324
+ );
325
+
326
+ -- Custom function (automatically passed through)
327
+ CREATE OR REPLACE FUNCTION assign_resource_to_user(
328
+ p_user_id INT,
329
+ p_resource_id INT,
330
+ p_target_user_id INT
331
+ ) RETURNS JSONB
332
+ LANGUAGE plpgsql SECURITY DEFINER AS $$
333
+ BEGIN
334
+ -- Check permission
335
+ IF NOT dzql.check_permission(p_user_id, 'update', 'resources',
336
+ (SELECT to_jsonb(resources.*) FROM resources WHERE id = p_resource_id)
337
+ ) THEN
338
+ RAISE EXCEPTION 'Permission denied';
339
+ END IF;
340
+
341
+ -- Update owner
342
+ UPDATE resources
343
+ SET owner_id = p_target_user_id
344
+ WHERE id = p_resource_id;
345
+
346
+ -- Return updated resource
347
+ RETURN (SELECT to_jsonb(resources.*) FROM resources WHERE id = p_resource_id);
348
+ END;
349
+ $$;
350
+
351
+ -- Register for RPC access
352
+ INSERT INTO dzql.registry (fn_regproc)
353
+ VALUES ('assign_resource_to_user'::regproc);
354
+ ```
355
+
356
+ After compilation, everything is in `init_db/resources.sql` - no manual copying needed!
357
+
358
+ ## See Also
359
+
360
+ - [Many-to-Many Support](./many-to-many.md) - M2M relationships eliminate many toggle functions
361
+ - [Field Defaults](./field-defaults.md) - Auto-populate fields
362
+ - [Graph Rules](../reference/api.md#graph-rules) - Automatic relationship management
@@ -0,0 +1,240 @@
1
+ # Field Defaults
2
+
3
+ Auto-populate fields with default values during entity creation.
4
+
5
+ ## Overview
6
+
7
+ Field defaults allow you to automatically set field values when creating new records, eliminating the need for clients to manually send fields like `owner_id`, `created_at`, or `status` on every save operation.
8
+
9
+ ## Benefits
10
+
11
+ - **Less Client Code** - No need to send the same fields repeatedly
12
+ - **Prevents Errors** - Can't forget required fields
13
+ - **Enforces Security** - Server controls defaults (e.g., current user as owner)
14
+ - **Cleaner API** - Focus on actual data, not boilerplate
15
+
16
+ ## Configuration
17
+
18
+ Field defaults are configured in the 10th parameter of `dzql.register_entity()`:
19
+
20
+ ```sql
21
+ SELECT dzql.register_entity(
22
+ 'resources',
23
+ 'title',
24
+ ARRAY['title'],
25
+ '{}', -- fk_includes
26
+ false, -- soft_delete
27
+ '{}', -- temporal_fields
28
+ '{}', -- notification_paths
29
+ '{}', -- permission_paths
30
+ '{}', -- graph_rules
31
+ '{
32
+ "owner_id": "@user_id",
33
+ "created_by": "@user_id",
34
+ "created_at": "@now",
35
+ "status": "draft"
36
+ }' -- field_defaults (10th parameter)
37
+ );
38
+ ```
39
+
40
+ ## Available Variables
41
+
42
+ Field defaults support special variables that are resolved at runtime:
43
+
44
+ | Variable | Value | Example Use Case |
45
+ |----------|-------|------------------|
46
+ | `@user_id` | Current user ID from `p_user_id` | Ownership, audit trails |
47
+ | `@now` | Current timestamp | `created_at`, `updated_at` |
48
+ | `@today` | Current date | `valid_from`, `date_created` |
49
+ | Literal values | Any JSON value | `"draft"`, `0`, `true` |
50
+
51
+ ## Behavior
52
+
53
+ ### INSERT Operations
54
+
55
+ Field defaults are **only applied during INSERT** (creating new records):
56
+
57
+ ```javascript
58
+ // Client doesn't send owner_id
59
+ await api.save_resources({
60
+ data: { title: "Conference Room A" }
61
+ })
62
+
63
+ // Server auto-populates:
64
+ // - owner_id = current user ID
65
+ // - created_at = current timestamp
66
+ // - status = "draft"
67
+ ```
68
+
69
+ ### UPDATE Operations
70
+
71
+ Field defaults are **NOT applied during UPDATE** (modifying existing records):
72
+
73
+ ```javascript
74
+ // Updating existing record
75
+ await api.save_resources({
76
+ data: {
77
+ id: 1,
78
+ title: "Updated Title"
79
+ }
80
+ })
81
+
82
+ // created_at is NOT changed
83
+ // owner_id is NOT changed
84
+ ```
85
+
86
+ ### Explicit Values Override Defaults
87
+
88
+ If the client explicitly provides a value, it takes precedence:
89
+
90
+ ```javascript
91
+ await api.save_resources({
92
+ data: {
93
+ title: "Room A",
94
+ status: "published" // Overrides "draft" default
95
+ }
96
+ })
97
+ ```
98
+
99
+ ## Common Use Cases
100
+
101
+ ### Ownership Tracking
102
+
103
+ ```sql
104
+ SELECT dzql.register_entity(
105
+ 'documents',
106
+ 'title',
107
+ ARRAY['title'],
108
+ '{}', false, '{}', '{}', '{}', '{}',
109
+ '{
110
+ "owner_id": "@user_id",
111
+ "created_by": "@user_id"
112
+ }'
113
+ );
114
+ ```
115
+
116
+ ### Timestamps
117
+
118
+ ```sql
119
+ SELECT dzql.register_entity(
120
+ 'posts',
121
+ 'title',
122
+ ARRAY['title'],
123
+ '{}', false, '{}', '{}', '{}', '{}',
124
+ '{
125
+ "created_at": "@now",
126
+ "published_at": "@now"
127
+ }'
128
+ );
129
+ ```
130
+
131
+ ### Status/Workflow
132
+
133
+ ```sql
134
+ SELECT dzql.register_entity(
135
+ 'orders',
136
+ 'order_number',
137
+ ARRAY['order_number'],
138
+ '{}', false, '{}', '{}', '{}', '{}',
139
+ '{
140
+ "status": "pending",
141
+ "priority": "normal",
142
+ "auto_process": "true"
143
+ }'
144
+ );
145
+ ```
146
+
147
+ ### Multi-Tenant
148
+
149
+ ```sql
150
+ SELECT dzql.register_entity(
151
+ 'items',
152
+ 'name',
153
+ ARRAY['name'],
154
+ '{}', false, '{}', '{}', '{}', '{}',
155
+ '{
156
+ "tenant_id": "@user_id",
157
+ "created_at": "@now",
158
+ "is_active": "true"
159
+ }'
160
+ );
161
+ ```
162
+
163
+ ## Security Considerations
164
+
165
+ Field defaults improve security by:
166
+
167
+ 1. **Preventing client-side tampering** - Server controls sensitive defaults
168
+ 2. **Enforcing ownership** - Can't set wrong `owner_id`
169
+ 3. **Audit trail integrity** - Timestamps set server-side
170
+ 4. **Consistent initialization** - Every record starts in known state
171
+
172
+ ## Example: Before vs After
173
+
174
+ ### Before (Manual)
175
+
176
+ ```javascript
177
+ // Client must remember to send owner_id every time
178
+ await api.save_tags({
179
+ data: {
180
+ name: "Important",
181
+ owner_id: user.id, // ← Easy to forget
182
+ created_at: new Date(), // ← Manual
183
+ status: "active" // ← Repetitive
184
+ }
185
+ })
186
+ ```
187
+
188
+ ### After (Automatic)
189
+
190
+ ```javascript
191
+ // Client sends only actual data
192
+ await api.save_tags({
193
+ data: {
194
+ name: "Important"
195
+ // owner_id, created_at, status auto-populated!
196
+ }
197
+ })
198
+ ```
199
+
200
+ ## Backwards Compatibility
201
+
202
+ Field defaults are **completely optional**:
203
+
204
+ - Entities without field defaults work exactly as before
205
+ - No migration needed for existing entities
206
+ - Can be added incrementally
207
+
208
+ ## Implementation Details
209
+
210
+ ### Storage
211
+
212
+ Field defaults are stored in the `dzql.entities` table:
213
+
214
+ ```sql
215
+ SELECT field_defaults FROM dzql.entities WHERE table_name = 'resources';
216
+ ```
217
+
218
+ Result:
219
+ ```json
220
+ {
221
+ "owner_id": "@user_id",
222
+ "created_at": "@now",
223
+ "status": "draft"
224
+ }
225
+ ```
226
+
227
+ ### Resolution
228
+
229
+ Variables are resolved in `generic_save()` using the existing `dzql.resolve_graph_variable()` function:
230
+
231
+ 1. Check if field is missing in `p_data`
232
+ 2. Get default value from entity config
233
+ 3. If starts with `@`, resolve the variable
234
+ 4. Add to data being inserted
235
+
236
+ ## See Also
237
+
238
+ - [Entity Registration](../reference/api.md#register_entity) - Full registration API
239
+ - [Many-to-Many Support](./many-to-many.md) - Relationship defaults
240
+ - [Custom Functions](./custom-functions.md) - Extending entities