dzql 0.5.33 → 0.6.1
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/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +309 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +653 -0
- package/docs/project-setup.md +456 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# Composite Primary Keys
|
|
2
|
-
|
|
3
|
-
DZQL supports tables with composite (compound) primary keys. This guide explains how to register entities with composite keys and how the generated CRUD functions work.
|
|
4
|
-
|
|
5
|
-
## When to Use Composite Keys
|
|
6
|
-
|
|
7
|
-
Composite primary keys are useful for:
|
|
8
|
-
|
|
9
|
-
- **Junction tables** with additional data (beyond simple M2M)
|
|
10
|
-
- **Position/state tables** keyed by entity type and ID
|
|
11
|
-
- **Multi-tenant tables** keyed by tenant + entity
|
|
12
|
-
- **Versioned records** keyed by ID + version
|
|
13
|
-
|
|
14
|
-
## Registering an Entity with a Composite Key
|
|
15
|
-
|
|
16
|
-
To register an entity with a composite primary key, add `primary_key` to the `graph_rules` parameter:
|
|
17
|
-
|
|
18
|
-
```sql
|
|
19
|
-
-- Create a table with composite primary key
|
|
20
|
-
CREATE TABLE canvas_positions (
|
|
21
|
-
entity_type VARCHAR(50) NOT NULL,
|
|
22
|
-
entity_id INTEGER NOT NULL,
|
|
23
|
-
x INTEGER NOT NULL,
|
|
24
|
-
y INTEGER NOT NULL,
|
|
25
|
-
width INTEGER DEFAULT 100,
|
|
26
|
-
height INTEGER DEFAULT 100,
|
|
27
|
-
PRIMARY KEY (entity_type, entity_id)
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
-- Register with DZQL using composite key
|
|
31
|
-
SELECT dzql.register_entity(
|
|
32
|
-
'canvas_positions', -- table_name
|
|
33
|
-
'entity_type', -- label_field
|
|
34
|
-
array['x', 'y', 'width', 'height'], -- searchable_fields
|
|
35
|
-
'{}', -- fk_includes
|
|
36
|
-
false, -- soft_delete
|
|
37
|
-
'{}', -- temporal_fields
|
|
38
|
-
'{}', -- notification_paths
|
|
39
|
-
jsonb_build_object( -- permission_paths
|
|
40
|
-
'view', array[]::text[],
|
|
41
|
-
'create', array[]::text[],
|
|
42
|
-
'update', array[]::text[],
|
|
43
|
-
'delete', array[]::text[]
|
|
44
|
-
),
|
|
45
|
-
jsonb_build_object( -- graph_rules
|
|
46
|
-
'primary_key', array['entity_type', 'entity_id']
|
|
47
|
-
)
|
|
48
|
-
);
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The `primary_key` array specifies the columns that form the composite key, in order.
|
|
52
|
-
|
|
53
|
-
## Generated Function Signatures
|
|
54
|
-
|
|
55
|
-
When you compile an entity with a composite primary key, the generated functions have different signatures:
|
|
56
|
-
|
|
57
|
-
### GET Function
|
|
58
|
-
|
|
59
|
-
```sql
|
|
60
|
-
-- Accepts JSONB with all PK fields
|
|
61
|
-
SELECT get_canvas_positions(
|
|
62
|
-
1, -- user_id
|
|
63
|
-
'{"entity_type": "node", "entity_id": 42}' -- composite PK as JSONB
|
|
64
|
-
);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### SAVE Function
|
|
68
|
-
|
|
69
|
-
```sql
|
|
70
|
-
-- Insert: provide all PK fields plus data
|
|
71
|
-
SELECT save_canvas_positions(
|
|
72
|
-
1, -- user_id
|
|
73
|
-
'{"entity_type": "node", "entity_id": 42, "x": 100, "y": 200}'
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
-- Update: same signature, existing record detected by PK
|
|
77
|
-
SELECT save_canvas_positions(
|
|
78
|
-
1,
|
|
79
|
-
'{"entity_type": "node", "entity_id": 42, "x": 150, "y": 250}'
|
|
80
|
-
);
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
The save function determines insert vs update by checking if a record with the composite key exists.
|
|
84
|
-
|
|
85
|
-
### DELETE Function
|
|
86
|
-
|
|
87
|
-
```sql
|
|
88
|
-
-- Accepts JSONB with all PK fields
|
|
89
|
-
SELECT delete_canvas_positions(
|
|
90
|
-
1, -- user_id
|
|
91
|
-
'{"entity_type": "node", "entity_id": 42}' -- composite PK as JSONB
|
|
92
|
-
);
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### SEARCH Function
|
|
96
|
-
|
|
97
|
-
Search works the same as simple PK entities - it returns paginated results with all fields.
|
|
98
|
-
|
|
99
|
-
## Type Casting
|
|
100
|
-
|
|
101
|
-
DZQL automatically determines type casting for PK columns:
|
|
102
|
-
|
|
103
|
-
- Columns named `id` or ending with `_id` are cast to `::int`
|
|
104
|
-
- Other columns (like `entity_type`) are left as text
|
|
105
|
-
|
|
106
|
-
This means for a key like `(entity_type, entity_id)`:
|
|
107
|
-
- `entity_type` is compared as text
|
|
108
|
-
- `entity_id` is cast to integer
|
|
109
|
-
|
|
110
|
-
## Events and Notifications
|
|
111
|
-
|
|
112
|
-
Events for composite PK entities include the full composite key in the `pk` field:
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"table_name": "canvas_positions",
|
|
117
|
-
"op": "insert",
|
|
118
|
-
"pk": {"entity_type": "node", "entity_id": 42},
|
|
119
|
-
"data": {"entity_type": "node", "entity_id": 42, "x": 100, "y": 200}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Limitations
|
|
124
|
-
|
|
125
|
-
- **M2M relationships**: Tables with composite PKs can have M2M relationships, but this is an advanced use case. The M2M sync uses the first PK column for junction table lookups.
|
|
126
|
-
- **Auto-increment**: Composite keys don't support auto-increment. All PK values must be provided on insert.
|
|
127
|
-
|
|
128
|
-
## Example: Template Dependencies
|
|
129
|
-
|
|
130
|
-
A practical example - tracking dependencies between templates:
|
|
131
|
-
|
|
132
|
-
```sql
|
|
133
|
-
CREATE TABLE template_dependencies (
|
|
134
|
-
template_id INTEGER NOT NULL REFERENCES templates(id),
|
|
135
|
-
depends_on_template_id INTEGER NOT NULL REFERENCES templates(id),
|
|
136
|
-
dependency_type VARCHAR(20) DEFAULT 'requires',
|
|
137
|
-
PRIMARY KEY (template_id, depends_on_template_id)
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
SELECT dzql.register_entity(
|
|
141
|
-
'template_dependencies',
|
|
142
|
-
'dependency_type',
|
|
143
|
-
array['dependency_type'],
|
|
144
|
-
'{"template": "templates", "depends_on": "templates"}',
|
|
145
|
-
false,
|
|
146
|
-
'{}',
|
|
147
|
-
'{}',
|
|
148
|
-
jsonb_build_object(
|
|
149
|
-
'view', array[]::text[],
|
|
150
|
-
'create', array[]::text[],
|
|
151
|
-
'update', array[]::text[],
|
|
152
|
-
'delete', array[]::text[]
|
|
153
|
-
),
|
|
154
|
-
jsonb_build_object(
|
|
155
|
-
'primary_key', array['template_id', 'depends_on_template_id']
|
|
156
|
-
)
|
|
157
|
-
);
|
|
158
|
-
```
|
|
@@ -1,362 +0,0 @@
|
|
|
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
|