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.
@@ -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
  '{
@@ -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 + M2M |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
5
5
  "type": "module",
6
6
  "main": "src/server/index.js",
@@ -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
- jsonb_build_object('id', (p_record->>'id')::int),
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
- jsonb_build_object('id', v_result.id),
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
- jsonb_build_object('id', v_result.id),
919
+ ${pkBuildObject},
903
920
  NULL,
904
921
  p_user_id,
905
922
  v_notify_users
@@ -258,7 +258,8 @@ export class DZQLCompiler {
258
258
  _generateGraphRuleFunctions(entity) {
259
259
  return generateGraphRuleFunctions(
260
260
  entity.tableName,
261
- entity.graphRules
261
+ entity.graphRules,
262
+ entity.primaryKey
262
263
  );
263
264
  }
264
265
 
@@ -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
  }