dzql 0.5.2 → 0.5.3
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/compiler/README.md +17 -0
- package/docs/reference/api.md +39 -1
- package/package.json +1 -1
- package/src/compiler/codegen/graph-rules-codegen.js +22 -4
- package/src/compiler/codegen/operation-codegen.js +19 -2
- package/src/compiler/compiler.js +2 -1
- package/src/compiler/parser/entity-parser.js +8 -1
package/docs/compiler/README.md
CHANGED
|
@@ -87,6 +87,23 @@ SELECT dzql.register_entity(
|
|
|
87
87
|
|
|
88
88
|
See [Many-to-Many Guide](../guides/many-to-many.md) for details.
|
|
89
89
|
|
|
90
|
+
### Composite Primary Keys
|
|
91
|
+
```sql
|
|
92
|
+
SELECT dzql.register_entity(
|
|
93
|
+
'product_task_template_dependencies', 'template_id', ARRAY[]::text[],
|
|
94
|
+
'{}', false, '{}', '{}', '{}',
|
|
95
|
+
'{
|
|
96
|
+
"primary_key": ["template_id", "depends_on_template_id"]
|
|
97
|
+
}',
|
|
98
|
+
'{}'
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Generated code:** Event records use all primary key columns
|
|
103
|
+
- Default assumes `id` column
|
|
104
|
+
- Composite keys generate `jsonb_build_object('col1', v_result.col1, 'col2', v_result.col2)`
|
|
105
|
+
- Required for junction tables and other composite PK scenarios
|
|
106
|
+
|
|
90
107
|
### Field Defaults
|
|
91
108
|
```sql
|
|
92
109
|
'{
|
package/docs/reference/api.md
CHANGED
|
@@ -252,9 +252,14 @@ SELECT dzql.register_entity(
|
|
|
252
252
|
| `p_temporal_fields` | JSONB | no | Temporal field config (valid_from/valid_to) |
|
|
253
253
|
| `p_notification_paths` | JSONB | no | Who receives real-time updates |
|
|
254
254
|
| `p_permission_paths` | JSONB | no | CRUD permission rules |
|
|
255
|
-
| `p_graph_rules` | JSONB | no | Automatic relationship management
|
|
255
|
+
| `p_graph_rules` | JSONB | no | Automatic relationship management, M2M, and primary_key |
|
|
256
256
|
| `p_field_defaults` | JSONB | no | Auto-populate fields on INSERT |
|
|
257
257
|
|
|
258
|
+
**Note:** `p_graph_rules` can include:
|
|
259
|
+
- `primary_key` - Array of column names for composite primary keys (default: `["id"]`)
|
|
260
|
+
- `many_to_many` - M2M relationship configurations
|
|
261
|
+
- `on_create`, `on_update`, `on_delete` - Graph rule triggers
|
|
262
|
+
|
|
258
263
|
### FK Includes
|
|
259
264
|
|
|
260
265
|
Configure which foreign keys to dereference in GET operations:
|
|
@@ -330,6 +335,39 @@ Auto-populate fields on INSERT with values or variables:
|
|
|
330
335
|
|
|
331
336
|
See [Field Defaults Guide](../guides/field-defaults.md) for details.
|
|
332
337
|
|
|
338
|
+
### Composite Primary Keys
|
|
339
|
+
|
|
340
|
+
For tables with composite primary keys (not just `id`), specify the primary key columns via `graph_rules.primary_key`:
|
|
341
|
+
|
|
342
|
+
```sql
|
|
343
|
+
'{
|
|
344
|
+
"primary_key": ["template_id", "depends_on_template_id"]
|
|
345
|
+
}'
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Or in JavaScript:**
|
|
349
|
+
```javascript
|
|
350
|
+
{
|
|
351
|
+
tableName: "product_task_template_dependencies",
|
|
352
|
+
primaryKey: ["template_id", "depends_on_template_id"],
|
|
353
|
+
// ...
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Default:** `["id"]` - assumes a single `id` column as primary key.
|
|
358
|
+
|
|
359
|
+
**Why this matters:** The compiler generates event records with a `pk` field containing the primary key values. Without this configuration, tables with composite primary keys would fail with "record has no field id" errors.
|
|
360
|
+
|
|
361
|
+
**Generated SQL (composite):**
|
|
362
|
+
```sql
|
|
363
|
+
jsonb_build_object('template_id', v_result.template_id, 'depends_on_template_id', v_result.depends_on_template_id)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Generated SQL (simple, default):**
|
|
367
|
+
```sql
|
|
368
|
+
jsonb_build_object('id', v_result.id)
|
|
369
|
+
```
|
|
370
|
+
|
|
333
371
|
### Many-to-Many Relationships
|
|
334
372
|
|
|
335
373
|
Configure M2M relationships via `graph_rules.many_to_many`:
|
package/package.json
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class GraphRulesCodegen {
|
|
7
|
-
constructor(tableName, graphRules) {
|
|
7
|
+
constructor(tableName, graphRules, primaryKey = ['id']) {
|
|
8
8
|
this.tableName = tableName;
|
|
9
9
|
this.graphRules = graphRules;
|
|
10
|
+
this.primaryKey = Array.isArray(primaryKey) ? primaryKey : [primaryKey];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -233,6 +234,20 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
233
234
|
PERFORM ${functionName}(${paramSQL});`;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Generate jsonb_build_object for primary key columns from a JSONB record variable
|
|
239
|
+
* Supports composite primary keys
|
|
240
|
+
* @param {string} recordVar - The JSONB record variable name (e.g., 'p_record')
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
_generatePKBuildObject(recordVar = 'p_record') {
|
|
244
|
+
// Build jsonb_build_object with all primary key columns
|
|
245
|
+
// e.g., jsonb_build_object('id', (p_record->>'id')::int)
|
|
246
|
+
// or jsonb_build_object('template_id', (p_record->>'template_id')::int, 'depends_on_template_id', (p_record->>'depends_on_template_id')::int)
|
|
247
|
+
const pairs = this.primaryKey.map(col => `'${col}', (${recordVar}->>'${col}')::int`);
|
|
248
|
+
return `jsonb_build_object(${pairs.join(', ')})`;
|
|
249
|
+
}
|
|
250
|
+
|
|
236
251
|
/**
|
|
237
252
|
* Generate NOTIFY action
|
|
238
253
|
* Creates an event that will be broadcast to specified users
|
|
@@ -298,6 +313,8 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
298
313
|
? `jsonb_build_object(${dataFields.join(', ')})`
|
|
299
314
|
: "'{}'::jsonb";
|
|
300
315
|
|
|
316
|
+
const pkBuildObject = this._generatePKBuildObject('p_record');
|
|
317
|
+
|
|
301
318
|
return `${comment}
|
|
302
319
|
-- Create notification event
|
|
303
320
|
INSERT INTO dzql.events (
|
|
@@ -310,7 +327,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
310
327
|
) VALUES (
|
|
311
328
|
'${this.tableName}',
|
|
312
329
|
'notify',
|
|
313
|
-
|
|
330
|
+
${pkBuildObject},
|
|
314
331
|
${dataSQL},
|
|
315
332
|
p_user_id,
|
|
316
333
|
${userIdSQL}
|
|
@@ -407,9 +424,10 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
407
424
|
* Generate graph rule functions for an entity
|
|
408
425
|
* @param {string} tableName - Table name
|
|
409
426
|
* @param {Object} graphRules - Graph rules object
|
|
427
|
+
* @param {Array<string>} primaryKey - Primary key column(s) (defaults to ['id'])
|
|
410
428
|
* @returns {string} SQL for graph rule functions
|
|
411
429
|
*/
|
|
412
|
-
export function generateGraphRuleFunctions(tableName, graphRules) {
|
|
413
|
-
const codegen = new GraphRulesCodegen(tableName, graphRules);
|
|
430
|
+
export function generateGraphRuleFunctions(tableName, graphRules, primaryKey = ['id']) {
|
|
431
|
+
const codegen = new GraphRulesCodegen(tableName, graphRules, primaryKey);
|
|
414
432
|
return codegen.generate();
|
|
415
433
|
}
|
|
@@ -855,12 +855,29 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
855
855
|
return calls.join('');
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Generate jsonb_build_object for primary key columns
|
|
860
|
+
* Supports composite primary keys by building an object with all pk columns
|
|
861
|
+
* @param {string} recordVar - The record variable name (e.g., 'v_result', 'v_existing')
|
|
862
|
+
* @private
|
|
863
|
+
*/
|
|
864
|
+
_generatePKBuildObject(recordVar = 'v_result') {
|
|
865
|
+
const primaryKey = this.entity.primaryKey || ['id'];
|
|
866
|
+
|
|
867
|
+
// Build jsonb_build_object with all primary key columns
|
|
868
|
+
// e.g., jsonb_build_object('id', v_result.id)
|
|
869
|
+
// or jsonb_build_object('template_id', v_result.template_id, 'depends_on_template_id', v_result.depends_on_template_id)
|
|
870
|
+
const pairs = primaryKey.map(col => `'${col}', ${recordVar}.${col}`);
|
|
871
|
+
return `jsonb_build_object(${pairs.join(', ')})`;
|
|
872
|
+
}
|
|
873
|
+
|
|
858
874
|
/**
|
|
859
875
|
* Generate notification SQL
|
|
860
876
|
* @private
|
|
861
877
|
*/
|
|
862
878
|
_generateNotificationSQL(operation = 'save') {
|
|
863
879
|
const hasNotificationPaths = this.entity.notificationPaths && Object.keys(this.entity.notificationPaths).length > 0;
|
|
880
|
+
const pkBuildObject = this._generatePKBuildObject('v_result');
|
|
864
881
|
|
|
865
882
|
if (operation === 'save') {
|
|
866
883
|
return `
|
|
@@ -878,7 +895,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
878
895
|
) VALUES (
|
|
879
896
|
'${this.tableName}',
|
|
880
897
|
CASE WHEN v_is_insert THEN 'insert' ELSE 'update' END,
|
|
881
|
-
|
|
898
|
+
${pkBuildObject},
|
|
882
899
|
v_output,
|
|
883
900
|
p_user_id,
|
|
884
901
|
v_notify_users
|
|
@@ -899,7 +916,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
899
916
|
) VALUES (
|
|
900
917
|
'${this.tableName}',
|
|
901
918
|
'delete',
|
|
902
|
-
|
|
919
|
+
${pkBuildObject},
|
|
903
920
|
NULL,
|
|
904
921
|
p_user_id,
|
|
905
922
|
v_notify_users
|
package/src/compiler/compiler.js
CHANGED
|
@@ -95,6 +95,9 @@ export class EntityParser {
|
|
|
95
95
|
// Extract many_to_many from graph_rules if present
|
|
96
96
|
const manyToMany = graphRules.many_to_many || {};
|
|
97
97
|
|
|
98
|
+
// Extract primary_key from graph_rules if present (defaults to ['id'])
|
|
99
|
+
const primaryKey = graphRules.primary_key || ['id'];
|
|
100
|
+
|
|
98
101
|
const config = {
|
|
99
102
|
tableName: this._cleanString(params[0]),
|
|
100
103
|
labelField: this._cleanString(params[1]),
|
|
@@ -106,7 +109,8 @@ export class EntityParser {
|
|
|
106
109
|
permissionPaths: params[7] ? this._parseJSON(params[7]) : {},
|
|
107
110
|
graphRules: graphRules,
|
|
108
111
|
fieldDefaults: params[9] ? this._parseJSON(params[9]) : {},
|
|
109
|
-
manyToMany: manyToMany
|
|
112
|
+
manyToMany: manyToMany,
|
|
113
|
+
primaryKey: Array.isArray(primaryKey) ? primaryKey : [primaryKey]
|
|
110
114
|
};
|
|
111
115
|
|
|
112
116
|
return config;
|
|
@@ -328,6 +332,8 @@ export class EntityParser {
|
|
|
328
332
|
parseFromObject(entity) {
|
|
329
333
|
const graphRules = entity.graphRules || {};
|
|
330
334
|
const manyToMany = entity.manyToMany || graphRules.many_to_many || {};
|
|
335
|
+
// Primary key can be specified directly on entity or in graphRules (defaults to ['id'])
|
|
336
|
+
const primaryKey = entity.primaryKey || graphRules.primary_key || ['id'];
|
|
331
337
|
|
|
332
338
|
return {
|
|
333
339
|
tableName: entity.tableName || entity.table,
|
|
@@ -341,6 +347,7 @@ export class EntityParser {
|
|
|
341
347
|
graphRules: graphRules,
|
|
342
348
|
fieldDefaults: entity.fieldDefaults || {},
|
|
343
349
|
manyToMany: manyToMany,
|
|
350
|
+
primaryKey: Array.isArray(primaryKey) ? primaryKey : [primaryKey],
|
|
344
351
|
customFunctions: entity.customFunctions || []
|
|
345
352
|
};
|
|
346
353
|
}
|