dzql 0.5.4 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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",
@@ -171,55 +171,62 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
171
171
 
172
172
  /**
173
173
  * Generate traversal check: @org_id->acts_for[org_id=$]{active}.user_id
174
+ * Supports multi-hop paths like: @product_id->products.organisation_id->acts_for[organisation_id=$].user_id
174
175
  * @private
175
176
  */
176
177
  _generateTraversalCheck(ast) {
177
178
  const steps = ast.steps;
178
179
 
179
- // Extract components from the path
180
- let sourceField = null;
181
- let targetTable = null;
182
- let targetField = null;
183
- let filters = [];
184
- let temporal = false;
185
-
186
- for (const step of steps) {
187
- if (step.type === 'field_ref') {
188
- if (!sourceField) {
189
- // First field reference is the source
190
- sourceField = step.field;
191
- } else {
192
- // Last field reference is the target
193
- targetField = step.field;
194
- }
195
- } else if (step.type === 'table_ref') {
196
- targetTable = step.table;
180
+ // First step should be the source field reference
181
+ if (steps.length === 0 || steps[0].type !== 'field_ref') {
182
+ return 'false';
183
+ }
197
184
 
198
- // Collect filter conditions
199
- if (step.filter) {
200
- filters = step.filter;
201
- }
185
+ const sourceField = steps[0].field;
202
186
 
203
- // Check for temporal marker
204
- if (step.temporal) {
205
- temporal = true;
206
- }
187
+ // Collect all table_ref steps (these are the hops)
188
+ const tableSteps = steps.filter(s => s.type === 'table_ref');
207
189
 
208
- // Get target field if specified in table ref
209
- if (step.targetField) {
210
- targetField = step.targetField;
211
- }
190
+ if (tableSteps.length === 0) {
191
+ return 'false';
192
+ }
193
+
194
+ // Build the value expression that resolves through intermediate tables
195
+ // Start with the record's source field
196
+ let valueExpr = `(p_record->>'${sourceField}')::int`;
197
+
198
+ // Process intermediate hops (all but the last table_ref)
199
+ // Each intermediate hop needs a subquery to resolve to the next field
200
+ for (let i = 0; i < tableSteps.length - 1; i++) {
201
+ const hop = tableSteps[i];
202
+ const table = hop.table;
203
+ const targetField = hop.targetField;
204
+
205
+ if (!targetField) {
206
+ // If no target field specified, assume 'id' for the lookup
207
+ // This shouldn't normally happen in well-formed paths
208
+ continue;
212
209
  }
210
+
211
+ // Build subquery: (SELECT targetField FROM table WHERE id = previousValue)
212
+ valueExpr = `(SELECT ${targetField} FROM ${table} WHERE id = ${valueExpr})`;
213
213
  }
214
214
 
215
- // Build WHERE conditions
215
+ // The last table_ref is where we do the EXISTS check
216
+ const finalStep = tableSteps[tableSteps.length - 1];
217
+ const targetTable = finalStep.table;
218
+ const targetField = finalStep.targetField;
219
+ const filters = finalStep.filter || [];
220
+ const temporal = finalStep.temporal || false;
221
+
222
+ // Build WHERE conditions for the final EXISTS query
216
223
  const conditions = [];
217
224
 
218
225
  // Add filter conditions
219
226
  for (const filter of filters) {
220
227
  if (filter.operator === '=' && filter.value.type === 'param') {
221
- // field=$ means match the record's field value
222
- conditions.push(`${targetTable}.${filter.field} = (p_record->>'${sourceField}')::int`);
228
+ // field=$ means match the resolved value from the path
229
+ conditions.push(`${targetTable}.${filter.field} = ${valueExpr}`);
223
230
  } else if (filter.operator === '=') {
224
231
  const value = this._formatValue(filter.value);
225
232
  conditions.push(`${targetTable}.${filter.field} = ${value}`);
@@ -228,9 +235,6 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
228
235
 
229
236
  // Add temporal condition
230
237
  if (temporal) {
231
- // Add temporal filtering for {active} marker
232
- // Assumes standard field names: valid_from and valid_to
233
- // This matches the interpreter's behavior in resolve_path_segment (002_functions.sql:316)
234
238
  conditions.push(`${targetTable}.valid_from <= NOW()`);
235
239
  conditions.push(`(${targetTable}.valid_to > NOW() OR ${targetTable}.valid_to IS NULL)`);
236
240
  }