feat: add $consider (UP/DOWN) toggle to query builder and evaluators
This commit is contained in:
parent
5328471229
commit
27c9044a8b
|
|
@ -50,6 +50,17 @@ pub struct Response {
|
|||
pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||
match query {
|
||||
Value::Object(map) => {
|
||||
// $consider — "up" (default) or "down": flips result if conditions match
|
||||
if let Some(consider) = map.get("$consider") {
|
||||
let is_down = consider.as_str() == Some("down");
|
||||
let rest: serde_json::Map<String, Value> = map.iter()
|
||||
.filter(|(k, _)| k.as_str() != "$consider")
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
let matches = evaluate(&Value::Object(rest), response).unwrap_or(false);
|
||||
return Ok(if is_down { !matches } else { matches });
|
||||
}
|
||||
|
||||
// $and / $or / $not
|
||||
if let Some(and) = map.get("$and") {
|
||||
let Value::Array(clauses) = and else { bail!("$and expects array") };
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue