dzql 0.5.7 → 0.5.9

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/bin/cli.js CHANGED
@@ -404,13 +404,12 @@ async function runMigrateNew(args) {
404
404
  }
405
405
 
406
406
  // Generate migration template
407
+ // Note: No BEGIN/COMMIT - postgres.js handles transactions automatically
407
408
  const template = `-- ============================================================================
408
409
  -- Migration ${paddedNumber}: ${migrationName.replace(/_/g, ' ')}
409
410
  -- Generated: ${new Date().toISOString().split('T')[0]}
410
411
  -- ============================================================================
411
412
 
412
- BEGIN;
413
-
414
413
  -- Part 1: Schema Changes
415
414
  -- ALTER TABLE example ADD COLUMN IF NOT EXISTS new_field TEXT;
416
415
 
@@ -440,8 +439,6 @@ BEGIN;
440
439
  -- ('my_custom_function'::regproc, 'Description of function')
441
440
  -- ON CONFLICT DO NOTHING;
442
441
 
443
- COMMIT;
444
-
445
442
  -- ============================================================================
446
443
  -- Rollback (for migrate:down support)
447
444
  -- ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
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",
@@ -167,12 +167,9 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
167
167
  return 'FALSE';
168
168
  }
169
169
 
170
- const startField = fieldRef.field;
171
170
  const targetTable = tableRef.table;
172
171
  const targetField = tableRef.targetField;
173
172
 
174
- const startValue = `(${recordVar}->>'${startField}')::int`;
175
-
176
173
  // Build WHERE clause
177
174
  const whereClauses = [];
178
175
 
@@ -181,8 +178,10 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
181
178
  for (const filterCondition of tableRef.filter) {
182
179
  const field = filterCondition.field;
183
180
  if (filterCondition.value.type === 'param') {
184
- // Parameter reference: org_id=$
185
- whereClauses.push(`${targetTable}.${field} = ${startValue}`);
181
+ // Parameter reference: org_id=$ means use the filter field name as the param key
182
+ // e.g., acts_for[organisation_id=$] -> (p_params->>'organisation_id')::int
183
+ const paramValue = `(${recordVar}->>'${field}')::int`;
184
+ whereClauses.push(`${targetTable}.${field} = ${paramValue}`);
186
185
  } else {
187
186
  // Literal value
188
187
  whereClauses.push(`${targetTable}.${field} = '${filterCondition.value}'`);
@@ -239,13 +238,20 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
239
238
  // Build relation subqueries
240
239
  const relationSelects = this._generateRelationSelects();
241
240
 
241
+ // Build schema with path mapping (baked in at compile time)
242
+ const pathMapping = this.buildPathMapping();
243
+ const schemaJson = JSON.stringify({
244
+ root: this.rootEntity,
245
+ paths: pathMapping
246
+ });
247
+
242
248
  return `CREATE OR REPLACE FUNCTION get_${this.name}(
243
249
  p_params JSONB,
244
250
  p_user_id INT
245
251
  ) RETURNS JSONB AS $$
246
252
  DECLARE
247
253
  ${paramDeclarations}
248
- v_result JSONB;
254
+ v_data JSONB;
249
255
  BEGIN
250
256
  -- Extract parameters
251
257
  ${paramExtractions}
@@ -259,11 +265,15 @@ ${paramExtractions}
259
265
  SELECT jsonb_build_object(
260
266
  '${this.rootEntity}', row_to_json(root.*)${relationSelects}
261
267
  )
262
- INTO v_result
268
+ INTO v_data
263
269
  FROM ${this.rootEntity} root
264
270
  WHERE ${rootFilter};
265
271
 
266
- RETURN v_result;
272
+ -- Return data with embedded schema for atomic updates
273
+ RETURN jsonb_build_object(
274
+ 'data', v_data,
275
+ 'schema', '${schemaJson}'::jsonb
276
+ );
267
277
  END;
268
278
  $$ LANGUAGE plpgsql SECURITY DEFINER;`;
269
279
  }
@@ -301,7 +311,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
301
311
  const relIncludes = typeof relConfig === 'object' ? relConfig.include : null;
302
312
 
303
313
  // Build filter condition
304
- let filterSQL = this._generateRelationFilter(relFilter, relEntity);
314
+ let filterSQL = this._generateRelationFilter(relFilter, relEntity, relConfig);
305
315
 
306
316
  // Build nested includes if any
307
317
  let nestedSelect = 'row_to_json(rel.*)';
@@ -334,11 +344,17 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
334
344
  /**
335
345
  * Generate filter for relation subquery
336
346
  * @private
347
+ * @param {string|null} filter - Optional filter expression
348
+ * @param {string} relEntity - Related entity table name
349
+ * @param {Object} relConfig - Full relation config (may contain foreignKey)
337
350
  */
338
- _generateRelationFilter(filter, relEntity) {
351
+ _generateRelationFilter(filter, relEntity, relConfig) {
339
352
  if (!filter) {
340
- // Default: foreign key to root
341
- return `rel.${this.rootEntity}_id = root.id`;
353
+ // Use explicit foreignKey from config, or default to root_id
354
+ const fk = (typeof relConfig === 'object' && relConfig.foreignKey)
355
+ ? relConfig.foreignKey
356
+ : `${this.rootEntity}_id`;
357
+ return `rel.${fk} = root.id`;
342
358
  }
343
359
 
344
360
  // Parse filter expression like "venue_id=$venue_id"
package/src/server/ws.js CHANGED
@@ -322,14 +322,28 @@ export function createRPCHandler(customHandlers = {}) {
322
322
  try {
323
323
  // Execute initial query (this also checks permissions)
324
324
  const queryResult = await sql.unsafe(
325
- `SELECT get_${subscribableName}($1, $2) as data`,
325
+ `SELECT get_${subscribableName}($1, $2) as result`,
326
326
  [params, ws.data.user_id]
327
327
  );
328
328
 
329
- const data = queryResult[0]?.data;
330
-
331
- // Get subscribable metadata for schema (path mapping for atomic updates)
332
- const metadata = await getSubscribableMetadata(subscribableName, sql);
329
+ const queryData = queryResult[0]?.result;
330
+
331
+ // Check if compiled function returned embedded schema
332
+ // Compiled: { data, schema } | Interpreted: just the data object
333
+ let data, schema;
334
+ if (queryData && queryData.schema && queryData.data !== undefined) {
335
+ // Compiled mode - schema is embedded
336
+ data = queryData.data;
337
+ schema = queryData.schema;
338
+ } else {
339
+ // Interpreted mode - fetch schema from metadata table
340
+ data = queryData;
341
+ const metadata = await getSubscribableMetadata(subscribableName, sql);
342
+ schema = {
343
+ root: metadata.rootEntity,
344
+ paths: metadata.pathMapping
345
+ };
346
+ }
333
347
 
334
348
  // Register subscription in memory
335
349
  const subscriptionId = registerSubscription(
@@ -343,11 +357,7 @@ export function createRPCHandler(customHandlers = {}) {
343
357
  const result = {
344
358
  subscription_id: subscriptionId,
345
359
  data,
346
- // Include schema for atomic update support
347
- schema: {
348
- root: metadata.rootEntity,
349
- paths: metadata.pathMapping
350
- }
360
+ schema
351
361
  };
352
362
 
353
363
  wsLogger.response(method, result, Date.now() - startTime);