dzql 0.2.3 → 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.
- package/docs/guides/custom-functions.md +362 -0
- package/docs/guides/field-defaults.md +240 -0
- package/docs/guides/many-to-many.md +894 -0
- package/docs/reference/api.md +147 -3
- package/package.json +2 -2
- package/src/compiler/compiler.js +23 -13
- package/src/compiler/parser/entity-parser.js +74 -14
- package/src/database/migrations/001_schema.sql +3 -1
- package/src/database/migrations/002_functions.sql +5 -0
- package/src/database/migrations/003_operations.sql +236 -1
- package/src/database/migrations/004_search.sql +64 -0
- package/src/database/migrations/005_entities.sql +11 -4
|
@@ -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
|