dzql 0.5.24 → 0.5.26
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/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Feature-specific guides and how-tos:
|
|
|
16
16
|
|
|
17
17
|
- **[Live Query Subscriptions](guides/subscriptions.md)** - Real-time denormalized documents
|
|
18
18
|
- **[Many-to-Many Relationships](guides/many-to-many.md)** - Junction table management
|
|
19
|
+
- **[Composite Primary Keys](guides/composite-primary-keys.md)** - Tables with compound keys
|
|
19
20
|
- **[Field Defaults](guides/field-defaults.md)** - Auto-populate fields on create
|
|
20
21
|
- **[Custom Functions](guides/custom-functions.md)** - Extend with PostgreSQL or Bun functions
|
|
21
22
|
- **[Client Stores](guides/client-stores.md)** - Pinia store patterns for Vue.js
|
|
@@ -23,10 +23,16 @@ Entity Registration:
|
|
|
23
23
|
temporal_fields, -- '{}'
|
|
24
24
|
notification_paths, -- '{"ownership": ["@org_id->acts_for..."]}'
|
|
25
25
|
permission_paths, -- '{"view": [], "create": [...]}'
|
|
26
|
-
graph_rules, -- '{"on_create": {...}, "many_to_many": {...}}'
|
|
26
|
+
graph_rules, -- '{"on_create": {...}, "many_to_many": {...}, "primary_key": [...]}'
|
|
27
27
|
field_defaults -- '{"owner_id": "@user_id"}'
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
Composite Primary Keys:
|
|
31
|
+
graph_rules: '{"primary_key": ["entity_type", "entity_id"]}'
|
|
32
|
+
- GET/DELETE accept JSONB: get_table(user_id, '{"col1": "val", "col2": 123}')
|
|
33
|
+
- SAVE detects insert/update by checking if all PK fields exist
|
|
34
|
+
- Columns ending with _id are cast to ::int, others stay text
|
|
35
|
+
|
|
30
36
|
M2M id_field naming: tag_ids (singular + _ids), NOT tags_ids
|
|
31
37
|
Permission [] = public, omitted = denied
|
|
32
38
|
Path syntax: @field->table[filter]{temporal}.target_field
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
```
|
package/package.json
CHANGED
|
@@ -462,6 +462,11 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
462
462
|
return `rel.${fk} = root.id`;
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// Dashboard collection: filter="TRUE" means fetch ALL rows (no FK filter)
|
|
466
|
+
if (filter === 'TRUE') {
|
|
467
|
+
return 'TRUE';
|
|
468
|
+
}
|
|
469
|
+
|
|
465
470
|
// Parse filter expression like "venue_id=$venue_id"
|
|
466
471
|
// Replace $param with v_param variable
|
|
467
472
|
return filter.replace(/\$(\w+)/g, 'v_$1');
|
|
@@ -529,10 +534,19 @@ $$ LANGUAGE plpgsql IMMUTABLE;`;
|
|
|
529
534
|
? relConfig.foreignKey
|
|
530
535
|
: `${this.rootEntity}_id`;
|
|
531
536
|
const relVia = typeof relConfig === 'object' ? relConfig.via : null;
|
|
537
|
+
const relFilter = typeof relConfig === 'object' ? relConfig.filter : null;
|
|
532
538
|
|
|
533
539
|
const params = Object.keys(this.paramSchema);
|
|
534
540
|
const firstParam = params[0] || 'id';
|
|
535
541
|
|
|
542
|
+
// Dashboard collection: filter="TRUE" means notify ALL subscribers
|
|
543
|
+
// This relation is independent from the root entity
|
|
544
|
+
if (relFilter === 'TRUE') {
|
|
545
|
+
return `-- Dashboard collection (${relEntity}) - notify all subscribers
|
|
546
|
+
WHEN '${relEntity}' THEN
|
|
547
|
+
v_affected := ARRAY['{}'::jsonb];`;
|
|
548
|
+
}
|
|
549
|
+
|
|
536
550
|
// Check if this is a nested relation (has parent FK)
|
|
537
551
|
const nestedIncludes = typeof relConfig === 'object' ? relConfig.include : null;
|
|
538
552
|
|