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
|
@@ -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
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
199
|
-
if (step.filter) {
|
|
200
|
-
filters = step.filter;
|
|
201
|
-
}
|
|
185
|
+
const sourceField = steps[0].field;
|
|
202
186
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
//
|
|
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
|
|
222
|
-
conditions.push(`${targetTable}.${filter.field} =
|
|
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
|
}
|