dzql 0.5.2 → 0.5.4
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 +64 -1
- package/package.json +1 -1
- package/src/compiler/codegen/graph-rules-codegen.js +22 -4
- package/src/compiler/codegen/operation-codegen.js +114 -2
- package/src/compiler/compiler.js +2 -1
- package/src/compiler/parser/entity-parser.js +8 -1
- package/src/server/db.js +34 -2
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,64 @@ 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
|
+
|
|
371
|
+
**API Usage with Composite Keys:**
|
|
372
|
+
|
|
373
|
+
For entities with composite primary keys, the `get` and `delete` operations accept a JSONB object instead of an integer `id`:
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
376
|
+
// Get by composite key
|
|
377
|
+
const dependency = await ws.api.get.product_task_template_dependencies({
|
|
378
|
+
template_id: 1,
|
|
379
|
+
depends_on_template_id: 2
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Delete by composite key
|
|
383
|
+
await ws.api.delete.product_task_template_dependencies({
|
|
384
|
+
template_id: 1,
|
|
385
|
+
depends_on_template_id: 2
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Or using explicit pk object
|
|
389
|
+
await ws.api.delete.product_task_template_dependencies({
|
|
390
|
+
pk: { template_id: 1, depends_on_template_id: 2 }
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Note:** Entities with simple `id` primary keys continue to use `{id: 1}` for backwards compatibility.
|
|
395
|
+
|
|
333
396
|
### Many-to-Many Relationships
|
|
334
397
|
|
|
335
398
|
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
|
}
|
|
@@ -7,6 +7,8 @@ export class OperationCodegen {
|
|
|
7
7
|
constructor(entity) {
|
|
8
8
|
this.entity = entity;
|
|
9
9
|
this.tableName = entity.tableName;
|
|
10
|
+
this.primaryKey = entity.primaryKey || ['id'];
|
|
11
|
+
this.isCompositePK = this.primaryKey.length > 1 || this.primaryKey[0] !== 'id';
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -31,6 +33,49 @@ export class OperationCodegen {
|
|
|
31
33
|
const m2mExpansionForGet = this._generateM2MExpansionForGet();
|
|
32
34
|
const filterSensitiveFields = this._generateSensitiveFieldFilter();
|
|
33
35
|
|
|
36
|
+
// For composite PKs, accept JSONB containing all PK fields
|
|
37
|
+
// For simple PKs, accept INT for backwards compatibility
|
|
38
|
+
if (this.isCompositePK) {
|
|
39
|
+
const whereClause = this.primaryKey.map(col => `${col} = (p_pk->>'${col}')::int`).join(' AND ');
|
|
40
|
+
const pkDescription = this.primaryKey.join(', ');
|
|
41
|
+
|
|
42
|
+
return `-- GET operation for ${this.tableName} (composite primary key: ${pkDescription})
|
|
43
|
+
CREATE OR REPLACE FUNCTION get_${this.tableName}(
|
|
44
|
+
p_user_id INT,
|
|
45
|
+
p_pk JSONB,
|
|
46
|
+
p_on_date TIMESTAMPTZ DEFAULT NULL
|
|
47
|
+
) RETURNS JSONB AS $$
|
|
48
|
+
DECLARE
|
|
49
|
+
v_result JSONB;
|
|
50
|
+
v_record ${this.tableName}%ROWTYPE;
|
|
51
|
+
BEGIN
|
|
52
|
+
-- Fetch the record by composite primary key
|
|
53
|
+
SELECT * INTO v_record
|
|
54
|
+
FROM ${this.tableName}
|
|
55
|
+
WHERE ${whereClause}${this._generateTemporalFilter()};
|
|
56
|
+
|
|
57
|
+
IF NOT FOUND THEN
|
|
58
|
+
RAISE EXCEPTION 'Record not found: % with pk=%', '${this.tableName}', p_pk;
|
|
59
|
+
END IF;
|
|
60
|
+
|
|
61
|
+
-- Convert to JSONB
|
|
62
|
+
v_result := to_jsonb(v_record);
|
|
63
|
+
|
|
64
|
+
-- Check view permission
|
|
65
|
+
IF NOT can_view_${this.tableName}(p_user_id, v_result) THEN
|
|
66
|
+
RAISE EXCEPTION 'Permission denied: view on ${this.tableName}';
|
|
67
|
+
END IF;
|
|
68
|
+
|
|
69
|
+
${fkExpansions}
|
|
70
|
+
${m2mExpansionForGet}
|
|
71
|
+
${filterSensitiveFields}
|
|
72
|
+
|
|
73
|
+
RETURN v_result;
|
|
74
|
+
END;
|
|
75
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Simple PK (id column) - original signature for backwards compatibility
|
|
34
79
|
return `-- GET operation for ${this.tableName}
|
|
35
80
|
CREATE OR REPLACE FUNCTION get_${this.tableName}(
|
|
36
81
|
p_user_id INT,
|
|
@@ -177,6 +222,56 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
177
222
|
const notificationSQL = this._generateNotificationSQL('delete');
|
|
178
223
|
const filterSensitiveFields = this._generateSensitiveFieldFilter('v_output');
|
|
179
224
|
|
|
225
|
+
// For composite PKs, accept JSONB containing all PK fields
|
|
226
|
+
if (this.isCompositePK) {
|
|
227
|
+
const whereClause = this.primaryKey.map(col => `${col} = (p_pk->>'${col}')::int`).join(' AND ');
|
|
228
|
+
const pkDescription = this.primaryKey.join(', ');
|
|
229
|
+
|
|
230
|
+
const deleteSQL = this.entity.softDelete
|
|
231
|
+
? `UPDATE ${this.tableName} SET deleted_at = NOW() WHERE ${whereClause} RETURNING * INTO v_result;`
|
|
232
|
+
: `DELETE FROM ${this.tableName} WHERE ${whereClause} RETURNING * INTO v_result;`;
|
|
233
|
+
|
|
234
|
+
return `-- DELETE operation for ${this.tableName} (composite primary key: ${pkDescription})
|
|
235
|
+
CREATE OR REPLACE FUNCTION delete_${this.tableName}(
|
|
236
|
+
p_user_id INT,
|
|
237
|
+
p_pk JSONB
|
|
238
|
+
) RETURNS JSONB AS $$
|
|
239
|
+
DECLARE
|
|
240
|
+
v_result ${this.tableName}%ROWTYPE;
|
|
241
|
+
v_output JSONB;
|
|
242
|
+
v_notify_users INT[];
|
|
243
|
+
BEGIN
|
|
244
|
+
-- Fetch record first by composite primary key
|
|
245
|
+
SELECT * INTO v_result
|
|
246
|
+
FROM ${this.tableName}
|
|
247
|
+
WHERE ${whereClause};
|
|
248
|
+
|
|
249
|
+
IF NOT FOUND THEN
|
|
250
|
+
RAISE EXCEPTION 'Record not found: % with pk=%', '${this.tableName}', p_pk;
|
|
251
|
+
END IF;
|
|
252
|
+
|
|
253
|
+
-- Check delete permission
|
|
254
|
+
IF NOT can_delete_${this.tableName}(p_user_id, to_jsonb(v_result)) THEN
|
|
255
|
+
RAISE EXCEPTION 'Permission denied: delete on ${this.tableName}';
|
|
256
|
+
END IF;
|
|
257
|
+
|
|
258
|
+
${graphRulesCall}
|
|
259
|
+
|
|
260
|
+
-- Perform delete
|
|
261
|
+
${deleteSQL}
|
|
262
|
+
|
|
263
|
+
${notificationSQL}
|
|
264
|
+
|
|
265
|
+
-- Prepare output (removing sensitive fields)
|
|
266
|
+
v_output := to_jsonb(v_result);
|
|
267
|
+
${filterSensitiveFields}
|
|
268
|
+
|
|
269
|
+
RETURN v_output;
|
|
270
|
+
END;
|
|
271
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Simple PK (id column) - original signature for backwards compatibility
|
|
180
275
|
const deleteSQL = this.entity.softDelete
|
|
181
276
|
? `UPDATE ${this.tableName} SET deleted_at = NOW() WHERE id = p_id RETURNING * INTO v_result;`
|
|
182
277
|
: `DELETE FROM ${this.tableName} WHERE id = p_id RETURNING * INTO v_result;`;
|
|
@@ -855,12 +950,29 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
855
950
|
return calls.join('');
|
|
856
951
|
}
|
|
857
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Generate jsonb_build_object for primary key columns
|
|
955
|
+
* Supports composite primary keys by building an object with all pk columns
|
|
956
|
+
* @param {string} recordVar - The record variable name (e.g., 'v_result', 'v_existing')
|
|
957
|
+
* @private
|
|
958
|
+
*/
|
|
959
|
+
_generatePKBuildObject(recordVar = 'v_result') {
|
|
960
|
+
const primaryKey = this.entity.primaryKey || ['id'];
|
|
961
|
+
|
|
962
|
+
// Build jsonb_build_object with all primary key columns
|
|
963
|
+
// e.g., jsonb_build_object('id', v_result.id)
|
|
964
|
+
// or jsonb_build_object('template_id', v_result.template_id, 'depends_on_template_id', v_result.depends_on_template_id)
|
|
965
|
+
const pairs = primaryKey.map(col => `'${col}', ${recordVar}.${col}`);
|
|
966
|
+
return `jsonb_build_object(${pairs.join(', ')})`;
|
|
967
|
+
}
|
|
968
|
+
|
|
858
969
|
/**
|
|
859
970
|
* Generate notification SQL
|
|
860
971
|
* @private
|
|
861
972
|
*/
|
|
862
973
|
_generateNotificationSQL(operation = 'save') {
|
|
863
974
|
const hasNotificationPaths = this.entity.notificationPaths && Object.keys(this.entity.notificationPaths).length > 0;
|
|
975
|
+
const pkBuildObject = this._generatePKBuildObject('v_result');
|
|
864
976
|
|
|
865
977
|
if (operation === 'save') {
|
|
866
978
|
return `
|
|
@@ -878,7 +990,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
878
990
|
) VALUES (
|
|
879
991
|
'${this.tableName}',
|
|
880
992
|
CASE WHEN v_is_insert THEN 'insert' ELSE 'update' END,
|
|
881
|
-
|
|
993
|
+
${pkBuildObject},
|
|
882
994
|
v_output,
|
|
883
995
|
p_user_id,
|
|
884
996
|
v_notify_users
|
|
@@ -899,7 +1011,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
899
1011
|
) VALUES (
|
|
900
1012
|
'${this.tableName}',
|
|
901
1013
|
'delete',
|
|
902
|
-
|
|
1014
|
+
${pkBuildObject},
|
|
903
1015
|
NULL,
|
|
904
1016
|
p_user_id,
|
|
905
1017
|
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
|
}
|
package/src/server/db.js
CHANGED
|
@@ -179,6 +179,20 @@ export async function setupListeners(callback) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// Helper to detect if args contains a composite primary key (pk object or multiple PK fields, no 'id')
|
|
183
|
+
function isCompositePK(args) {
|
|
184
|
+
// If args has a 'pk' object, it's explicitly a composite PK call
|
|
185
|
+
if (args.pk && typeof args.pk === 'object') {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
// If args has no 'id' field but has other fields, assume composite PK
|
|
189
|
+
// (the compiled function will validate the actual PK fields)
|
|
190
|
+
if (args.id === undefined && Object.keys(args).length > 0) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
// DZQL Generic Operations - Try compiled functions first, fall back to generic_exec
|
|
183
197
|
export async function callDZQLOperation(operation, entity, args, userId) {
|
|
184
198
|
dbLogger.trace(`DZQL ${operation}.${entity} for user ${userId}`);
|
|
@@ -189,9 +203,9 @@ export async function callDZQLOperation(operation, entity, args, userId) {
|
|
|
189
203
|
// Try compiled function first
|
|
190
204
|
// Different operations have different signatures:
|
|
191
205
|
// - search: search_entity(p_user_id, p_filters, p_search, p_sort, p_page, p_limit)
|
|
192
|
-
// - get: get_entity(p_user_id, p_id, p_on_date)
|
|
206
|
+
// - get: get_entity(p_user_id, p_id, p_on_date) OR get_entity(p_user_id, p_pk, p_on_date) for composite PKs
|
|
193
207
|
// - save: save_entity(p_user_id, p_data, p_on_date)
|
|
194
|
-
// - delete: delete_entity(p_user_id, p_id)
|
|
208
|
+
// - delete: delete_entity(p_user_id, p_id) OR delete_entity(p_user_id, p_pk) for composite PKs
|
|
195
209
|
// - lookup: lookup_entity(p_user_id, p_term, p_limit)
|
|
196
210
|
|
|
197
211
|
if (operation === 'search') {
|
|
@@ -207,6 +221,15 @@ export async function callDZQLOperation(operation, entity, args, userId) {
|
|
|
207
221
|
`, [userId, filters, search, sort, page, limit]);
|
|
208
222
|
return result[0].result;
|
|
209
223
|
} else if (operation === 'get') {
|
|
224
|
+
// Support composite primary keys: pass pk object or full args as JSONB
|
|
225
|
+
if (isCompositePK(args)) {
|
|
226
|
+
const pk = args.pk || args; // Use explicit pk object or treat all args as PK fields
|
|
227
|
+
const result = await sql.unsafe(`
|
|
228
|
+
SELECT ${compiledFunctionName}($1::int, $2::jsonb, NULL) as result
|
|
229
|
+
`, [userId, pk]);
|
|
230
|
+
return result[0].result;
|
|
231
|
+
}
|
|
232
|
+
// Simple PK (id)
|
|
210
233
|
const result = await sql.unsafe(`
|
|
211
234
|
SELECT ${compiledFunctionName}($1::int, $2::int, NULL) as result
|
|
212
235
|
`, [userId, args.id]);
|
|
@@ -217,6 +240,15 @@ export async function callDZQLOperation(operation, entity, args, userId) {
|
|
|
217
240
|
`, [userId, args]);
|
|
218
241
|
return result[0].result;
|
|
219
242
|
} else if (operation === 'delete') {
|
|
243
|
+
// Support composite primary keys: pass pk object or full args as JSONB
|
|
244
|
+
if (isCompositePK(args)) {
|
|
245
|
+
const pk = args.pk || args; // Use explicit pk object or treat all args as PK fields
|
|
246
|
+
const result = await sql.unsafe(`
|
|
247
|
+
SELECT ${compiledFunctionName}($1::int, $2::jsonb) as result
|
|
248
|
+
`, [userId, pk]);
|
|
249
|
+
return result[0].result;
|
|
250
|
+
}
|
|
251
|
+
// Simple PK (id)
|
|
220
252
|
const result = await sql.unsafe(`
|
|
221
253
|
SELECT ${compiledFunctionName}($1::int, $2::int) as result
|
|
222
254
|
`, [userId, args.id]);
|