dzql 0.6.32 → 0.6.33
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
|
@@ -88,34 +88,74 @@ BEGIN
|
|
|
88
88
|
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
89
89
|
|
|
90
90
|
-- Build WHERE clause from filters
|
|
91
|
+
-- Note: We avoid ::TEXT casts to preserve index usage on numeric/date columns
|
|
92
|
+
-- The %L format specifier handles proper escaping for all types
|
|
91
93
|
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
92
94
|
LOOP
|
|
93
95
|
-- Handle simple value (exact match)
|
|
94
|
-
IF jsonb_typeof(v_filter)
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
IF jsonb_typeof(v_filter) = 'number' THEN
|
|
97
|
+
-- Numeric: compare without casting
|
|
98
|
+
v_where_clause := v_where_clause || format(' AND %I = %s', v_field, v_filter);
|
|
99
|
+
ELSIF jsonb_typeof(v_filter) = 'boolean' THEN
|
|
100
|
+
-- Boolean: compare directly
|
|
101
|
+
v_where_clause := v_where_clause || format(' AND %I = %s', v_field, v_filter);
|
|
102
|
+
ELSIF jsonb_typeof(v_filter) = 'string' THEN
|
|
103
|
+
-- String: use proper quoting
|
|
104
|
+
v_where_clause := v_where_clause || format(' AND %I = %L', v_field, v_filter #>> '{}');
|
|
105
|
+
ELSIF jsonb_typeof(v_filter) = 'object' THEN
|
|
97
106
|
-- Handle operator-based filters
|
|
98
107
|
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
99
108
|
LOOP
|
|
100
109
|
CASE v_operator
|
|
101
110
|
WHEN 'eq' THEN
|
|
102
|
-
|
|
111
|
+
IF jsonb_typeof(v_value) = 'number' THEN
|
|
112
|
+
v_where_clause := v_where_clause || format(' AND %I = %s', v_field, v_value);
|
|
113
|
+
ELSE
|
|
114
|
+
v_where_clause := v_where_clause || format(' AND %I = %L', v_field, v_value #>> '{}');
|
|
115
|
+
END IF;
|
|
103
116
|
WHEN 'ne' THEN
|
|
104
|
-
|
|
117
|
+
IF jsonb_typeof(v_value) = 'number' THEN
|
|
118
|
+
v_where_clause := v_where_clause || format(' AND %I != %s', v_field, v_value);
|
|
119
|
+
ELSE
|
|
120
|
+
v_where_clause := v_where_clause || format(' AND %I != %L', v_field, v_value #>> '{}');
|
|
121
|
+
END IF;
|
|
105
122
|
WHEN 'gt' THEN
|
|
106
|
-
|
|
123
|
+
IF jsonb_typeof(v_value) = 'number' THEN
|
|
124
|
+
v_where_clause := v_where_clause || format(' AND %I > %s', v_field, v_value);
|
|
125
|
+
ELSE
|
|
126
|
+
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
127
|
+
END IF;
|
|
107
128
|
WHEN 'gte' THEN
|
|
108
|
-
|
|
129
|
+
IF jsonb_typeof(v_value) = 'number' THEN
|
|
130
|
+
v_where_clause := v_where_clause || format(' AND %I >= %s', v_field, v_value);
|
|
131
|
+
ELSE
|
|
132
|
+
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
133
|
+
END IF;
|
|
109
134
|
WHEN 'lt' THEN
|
|
110
|
-
|
|
135
|
+
IF jsonb_typeof(v_value) = 'number' THEN
|
|
136
|
+
v_where_clause := v_where_clause || format(' AND %I < %s', v_field, v_value);
|
|
137
|
+
ELSE
|
|
138
|
+
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
139
|
+
END IF;
|
|
111
140
|
WHEN 'lte' THEN
|
|
112
|
-
|
|
141
|
+
IF jsonb_typeof(v_value) = 'number' THEN
|
|
142
|
+
v_where_clause := v_where_clause || format(' AND %I <= %s', v_field, v_value);
|
|
143
|
+
ELSE
|
|
144
|
+
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
145
|
+
END IF;
|
|
113
146
|
WHEN 'in' THEN
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
-- For IN, we need to handle arrays - cast elements appropriately
|
|
148
|
+
IF jsonb_typeof(v_value->0) = 'number' THEN
|
|
149
|
+
v_where_clause := v_where_clause || format(' AND %I = ANY(ARRAY(SELECT (jsonb_array_elements(%L))::int))', v_field, v_value);
|
|
150
|
+
ELSE
|
|
151
|
+
v_where_clause := v_where_clause || format(' AND %I = ANY(ARRAY(SELECT jsonb_array_elements_text(%L)))', v_field, v_value);
|
|
152
|
+
END IF;
|
|
116
153
|
WHEN 'not_in' THEN
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
IF jsonb_typeof(v_value->0) = 'number' THEN
|
|
155
|
+
v_where_clause := v_where_clause || format(' AND %I != ALL(ARRAY(SELECT (jsonb_array_elements(%L))::int))', v_field, v_value);
|
|
156
|
+
ELSE
|
|
157
|
+
v_where_clause := v_where_clause || format(' AND %I != ALL(ARRAY(SELECT jsonb_array_elements_text(%L)))', v_field, v_value);
|
|
158
|
+
END IF;
|
|
119
159
|
WHEN 'like' THEN
|
|
120
160
|
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
121
161
|
WHEN 'ilike' THEN
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { SubscribableIR, IncludeIR, EntityIR } from "../../shared/ir.js";
|
|
12
|
+
import { parsePath } from "../compiler/paths.js";
|
|
12
13
|
|
|
13
14
|
export function generateSubscribableSQL(name: string, sub: SubscribableIR, entities: Record<string, EntityIR> = {}): string {
|
|
14
15
|
// Validate: if root.key is set, it must exist in params or start with '@'
|
|
@@ -144,6 +145,7 @@ $$;`;
|
|
|
144
145
|
/**
|
|
145
146
|
* Compile permission for subscribable can_subscribe function.
|
|
146
147
|
* Uses RECORD dot notation since v_root is a RECORD, not JSONB.
|
|
148
|
+
* Supports unlimited traversal depth using the shared path parser.
|
|
147
149
|
*
|
|
148
150
|
* @param entityName - The root entity name
|
|
149
151
|
* @param rule - The permission rule (e.g., "@org_id->acts_for[org_id=$]{active}.user_id")
|
|
@@ -156,69 +158,79 @@ function compileSubscribePermission(entityName: string, rule: string, rootKey: s
|
|
|
156
158
|
return field === rootKey ? 'id' : field;
|
|
157
159
|
};
|
|
158
160
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// The acts_for join uses the original field name (org_id), but v_root uses resolved field (id)
|
|
166
|
-
return `EXISTS (SELECT 1 FROM acts_for WHERE acts_for.${field} = v_root.${rootField} AND acts_for.user_id = p_user_id AND acts_for.active)`;
|
|
161
|
+
// Parse the path using the shared parser
|
|
162
|
+
const parsed = parsePath(rule);
|
|
163
|
+
|
|
164
|
+
if (!parsed.isValid) {
|
|
165
|
+
console.warn(`[Compiler] Warning: Invalid permission path '${rule}': ${parsed.error}`);
|
|
166
|
+
return 'FALSE';
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
//
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const rootField = resolveField(startField);
|
|
169
|
+
// TRUE/FALSE literals
|
|
170
|
+
if (parsed.hops.length === 0) {
|
|
171
|
+
return rule === 'TRUE' || rule === 'true' ? 'TRUE' : 'FALSE';
|
|
172
|
+
}
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
174
|
+
// Simple field reference: @org_id implies checking acts_for membership
|
|
175
|
+
if (parsed.hops.length === 1 && parsed.hops[0].type === 'field') {
|
|
176
|
+
const field = parsed.hops[0].field!;
|
|
177
|
+
const rootField = resolveField(field);
|
|
178
|
+
return `EXISTS (SELECT 1 FROM acts_for WHERE acts_for.${field} = v_root.${rootField} AND acts_for.user_id = p_user_id AND acts_for.active)`;
|
|
179
|
+
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
// Traversal path - build EXISTS with nested subqueries for unlimited hops
|
|
182
|
+
// Start with the field from v_root
|
|
183
|
+
const startField = parsed.startField!;
|
|
184
|
+
const rootField = resolveField(startField);
|
|
182
185
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const targetTable = tableMatch[1];
|
|
186
|
+
// Build value expression starting from v_root
|
|
187
|
+
let valueExpr = `v_root.${rootField}`;
|
|
186
188
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
189
|
+
// Process intermediate hops (all but first field and last final)
|
|
190
|
+
for (let i = 1; i < parsed.hops.length - 1; i++) {
|
|
191
|
+
const hop = parsed.hops[i];
|
|
192
|
+
if (hop.type === 'table' && hop.table && hop.outputField) {
|
|
193
|
+
valueExpr = `(SELECT ${hop.outputField} FROM ${hop.table} WHERE id = ${valueExpr})`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
// Final hop - build EXISTS check
|
|
198
|
+
const finalHop = parsed.hops[parsed.hops.length - 1];
|
|
199
|
+
if (finalHop.type !== 'final' || !finalHop.table) {
|
|
200
|
+
return 'FALSE';
|
|
201
|
+
}
|
|
194
202
|
|
|
195
|
-
|
|
203
|
+
const conditions: string[] = [];
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Implicit join: table.id = v_root.field
|
|
204
|
-
sql += `${targetTable}.id = v_root.${rootField}`;
|
|
205
|
-
}
|
|
205
|
+
// Join condition
|
|
206
|
+
if (finalHop.filter && finalHop.filter.value === '$') {
|
|
207
|
+
conditions.push(`${finalHop.table}.${finalHop.filter.field} = ${valueExpr}`);
|
|
208
|
+
} else {
|
|
209
|
+
conditions.push(`${finalHop.table}.id = ${valueExpr}`);
|
|
210
|
+
}
|
|
206
211
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
// User check
|
|
213
|
+
if (finalHop.outputField === 'user_id') {
|
|
214
|
+
conditions.push(`${finalHop.table}.user_id = p_user_id`);
|
|
215
|
+
} else if (finalHop.outputField) {
|
|
216
|
+
conditions.push(`${finalHop.table}.${finalHop.outputField} = p_user_id`);
|
|
217
|
+
}
|
|
211
218
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
// Additional conditions from {active} or {role=admin}
|
|
220
|
+
for (const cond of finalHop.conditions || []) {
|
|
221
|
+
if (cond.type === 'boolean') {
|
|
222
|
+
conditions.push(`${finalHop.table}.${cond.field} = true`);
|
|
223
|
+
} else if (cond.type === 'equality') {
|
|
224
|
+
if (cond.value === 'NULL') {
|
|
225
|
+
conditions.push(`${finalHop.table}.${cond.field} IS NULL`);
|
|
226
|
+
} else {
|
|
227
|
+
const value = cond.value!.match(/^\d+$/) ? cond.value : `'${cond.value}'`;
|
|
228
|
+
conditions.push(`${finalHop.table}.${cond.field} = ${value}`);
|
|
229
|
+
}
|
|
215
230
|
}
|
|
216
|
-
|
|
217
|
-
sql += `)`;
|
|
218
|
-
return sql;
|
|
219
231
|
}
|
|
220
232
|
|
|
221
|
-
return '
|
|
233
|
+
return `EXISTS (SELECT 1 FROM ${finalHop.table} WHERE ${conditions.join(' AND ')})`;
|
|
222
234
|
}
|
|
223
235
|
|
|
224
236
|
/** Column info from EntityIR */
|