From 462deaa731d7096f668d22c9c6cf9019df7362b4 Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Fri, 27 Mar 2026 07:38:15 +0000 Subject: [PATCH] fix: nested _where filters fail open when intermediate field is missing Fixes #1731 When using nested object predicates like { title: { nested: { eq: 'zzz' } } }, the filter would incorrectly return all rows if the intermediate field (title) was not an object. This happened because the code would 'continue' instead of returning false when the field was missing or not an object. The fix adds a hasKnownOperatorsAtAnyLevel() helper to detect when a nested object contains known operators at any depth. If it does, and the field is not an object, the filter now correctly returns false. This preserves the existing fail-open behavior for unknown operators while fixing the fail-closed behavior for nested predicates on non-object fields. - Added hasKnownOperatorsAtAnyLevel() helper function - Modified nested object handling to check for operators at any level - Added regression tests for the specific bug cases --- src/matches-where.test.ts | 5 +++++ src/matches-where.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/matches-where.test.ts b/src/matches-where.test.ts index 26a2be4a9..8f8fca125 100644 --- a/src/matches-where.test.ts +++ b/src/matches-where.test.ts @@ -53,6 +53,11 @@ await test('matchesWhere', async (t) => { [{ c: { endsWith: 'z' } }, false], [{ a: { endsWith: '1' } }, false], [{ c: { endsWith: 1 } }, false], + // Regression tests for issue #1731: nested _where fail-open + // When intermediate field is not an object, nested predicates should fail (return false) + [{ a: { nested: { eq: 'zzz' } } }, false], // a is a number, not an object + [{ c: { nested: { eq: 'zzz' } } }, false], // c is a string, not an object + [{ or: [{ a: { nested: { eq: 'zzz' } } }] }, false], ] for (const [query, expected] of cases) { diff --git a/src/matches-where.ts b/src/matches-where.ts index 036b00c69..ee3b8a60f 100644 --- a/src/matches-where.ts +++ b/src/matches-where.ts @@ -21,6 +21,19 @@ function getKnownOperators(value: unknown): WhereOperator[] { return ops } +function hasKnownOperatorsAtAnyLevel(value: unknown): boolean { + if (!isJSONObject(value)) return false + + const knownOps = getKnownOperators(value) + if (knownOps.length > 0) return true + + for (const v of Object.values(value)) { + if (hasKnownOperatorsAtAnyLevel(v)) return true + } + + return false +} + export function matchesWhere(obj: JsonObject, where: JsonObject): boolean { for (const [key, value] of Object.entries(where)) { if (key === 'or') { @@ -74,6 +87,8 @@ export function matchesWhere(obj: JsonObject, where: JsonObject): boolean { if (isJSONObject(field)) { if (!matchesWhere(field, value)) return false + } else if (hasKnownOperatorsAtAnyLevel(value)) { + return false } continue