(obj, expr, args)
| 312 | // Evaluate a Realm query expression against a plain object. |
| 313 | // Handles: `field == $N`, `field IN {$0,$1,...}`, AND, OR, and parens. |
| 314 | const evalExpr = (obj, expr, args) => { |
| 315 | expr = expr.trim(); |
| 316 | // Strip matching outer parens — e.g. "(a == $0 OR a == $1)" → "a == $0 OR a == $1" |
| 317 | while (expr.startsWith('(') && expr.endsWith(')')) { |
| 318 | let depth = 0; |
| 319 | let allWrapped = true; |
| 320 | for (let i = 0; i < expr.length - 1; i++) { |
| 321 | if (expr[i] === '(') depth++; |
| 322 | else if (expr[i] === ')') { |
| 323 | if (--depth === 0) { |
| 324 | allWrapped = false; |
| 325 | break; |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | if (allWrapped) expr = expr.slice(1, -1).trim(); |
| 330 | else break; |
| 331 | } |
| 332 | // AND: all sub-expressions must match |
| 333 | const andParts = splitTop(expr, ' AND '); |
| 334 | if (andParts.length > 1) return andParts.every(p => evalExpr(obj, p, args)); |
| 335 | // OR: any sub-expression must match |
| 336 | const orParts = splitTop(expr, ' OR '); |
| 337 | if (orParts.length > 1) return orParts.some(p => evalExpr(obj, p, args)); |
| 338 | // IN {$0, $1, ...} — used by BoltzSwap repository |
| 339 | const inMatch = expr.match(/^(\w+)\s+IN\s+\{([^}]*)\}$/i); |
| 340 | if (inMatch) { |
| 341 | const field = inMatch[1]; |
| 342 | const values = inMatch[2].split(',').map(p => { |
| 343 | const m = p.trim().match(/^\$(\d+)$/); |
| 344 | return m ? args[+m[1]] : undefined; |
| 345 | }); |
| 346 | return values.includes(obj[field]); |
| 347 | } |
| 348 | // field == $N |
| 349 | const eqMatch = expr.match(/^(\w+)\s*==\s*\$(\d+)$/); |
| 350 | if (eqMatch) return obj[eqMatch[1]] === args[+eqMatch[2]]; |
| 351 | return true; // unknown expression — pass through |
| 352 | }; |
| 353 | |
| 354 | // Build a chainable collection over an array of Realm objects. |
| 355 | const makeCollection = (type, items) => { |
no test coverage detected