feat: add $consider (UP/DOWN) toggle to query builder and evaluators

This commit is contained in:
M1
2026-03-16 13:56:36 +04:00
parent 5328471229
commit 27c9044a8b
3 changed files with 46 additions and 9 deletions
+27 -9
View File
@@ -21,6 +21,7 @@ class QueryBuilder {
this.container = container;
this.onChange = onChange;
this.logic = '$and';
this.consider = 'up'; // 'up' | 'down'
this.rules = [this._emptyRule()];
this.render();
}
@@ -34,8 +35,9 @@ class QueryBuilder {
.map(r => this._ruleToQuery(r))
.filter(Boolean);
if (conditions.length === 0) return null;
if (conditions.length === 1) return conditions[0];
return { [this.logic]: conditions };
const base = conditions.length === 1 ? conditions[0] : { [this.logic]: conditions };
if (this.consider === 'down') return { $consider: 'down', ...base };
return base;
}
_ruleToQuery(rule) {
@@ -86,18 +88,23 @@ class QueryBuilder {
if (!query || typeof query !== 'object') {
this.rules = [this._emptyRule()];
this.logic = '$and';
this.consider = 'up';
this.render();
return;
}
if ('$and' in query || '$or' in query) {
this.logic = '$and' in query ? '$and' : '$or';
const clauses = query[this.logic];
// Strip $consider before parsing rules
this.consider = query.$consider === 'down' ? 'down' : 'up';
const q = Object.fromEntries(Object.entries(query).filter(([k]) => k !== '$consider'));
if ('$and' in q || '$or' in q) {
this.logic = '$and' in q ? '$and' : '$or';
const clauses = q[this.logic];
if (Array.isArray(clauses)) {
this.rules = clauses.map(c => this._queryToRule(c)).filter(Boolean);
}
} else {
this.rules = [this._queryToRule(query)].filter(Boolean);
this.rules = [this._queryToRule(q)].filter(Boolean);
}
if (this.rules.length === 0) this.rules = [this._emptyRule()];
@@ -164,13 +171,18 @@ class QueryBuilder {
this.container.innerHTML = `
<div class="space-y-3">
<div class="flex items-center gap-2 mb-4">
<span class="text-sm text-gray-400">Match</span>
<div class="flex items-center gap-2 mb-4 flex-wrap">
<span class="text-sm text-gray-400">Consider</span>
<select id="qb-consider" class="bg-gray-800 border border-gray-700 text-sm rounded px-2 py-1 focus:border-blue-500 focus:outline-none font-medium ${this.consider === 'down' ? 'text-red-400' : 'text-green-400'}">
<option value="up" ${this.consider === 'up' ? 'selected' : ''}>UP</option>
<option value="down" ${this.consider === 'down' ? 'selected' : ''}>DOWN</option>
</select>
<span class="text-sm text-gray-400">when</span>
<select id="qb-logic" class="bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded px-2 py-1 focus:border-blue-500 focus:outline-none">
<option value="$and" ${this.logic === '$and' ? 'selected' : ''}>ALL</option>
<option value="$or" ${this.logic === '$or' ? 'selected' : ''}>ANY</option>
</select>
<span class="text-sm text-gray-400">of the following conditions</span>
<span class="text-sm text-gray-400">of the following match</span>
</div>
<div id="qb-rules" class="space-y-2">
@@ -190,6 +202,12 @@ class QueryBuilder {
`;
// Bind events
this.container.querySelector('#qb-consider').addEventListener('change', (e) => {
this.consider = e.target.value;
this.render();
this.onChange?.(this.getQuery());
});
this.container.querySelector('#qb-logic').addEventListener('change', (e) => {
this.logic = e.target.value;
this.render();
+8
View File
@@ -86,6 +86,14 @@ export function evaluate(query: unknown, ctx: EvalContext): boolean {
const q = query as Record<string, unknown>;
// $consider — "up" (default) or "down": flips result if conditions match
if ("$consider" in q) {
const consider = q.$consider as string;
const rest = Object.fromEntries(Object.entries(q).filter(([k]) => k !== "$consider"));
const matches = evaluate(rest, ctx);
return consider === "down" ? !matches : matches;
}
// $and
if ("$and" in q) {
const clauses = q.$and;