From bc524d0b531faeea95197ff416d33a36213e2abc Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 18 Jul 2026 00:36:25 +0200 Subject: [PATCH] fix(176): split fulltext matches/notMatches from text contains --- web/src/builder/rules/compile.test.ts | 5 +++++ web/src/builder/rules/compile.ts | 4 ++-- web/src/builder/rules/types.ts | 9 +++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/web/src/builder/rules/compile.test.ts b/web/src/builder/rules/compile.test.ts index 0b92c9364..14c28f15b 100644 --- a/web/src/builder/rules/compile.test.ts +++ b/web/src/builder/rules/compile.test.ts @@ -18,6 +18,11 @@ describe('compile', () => { expect(compile({ match: 'all', children: [{ field: 'title', operator: 'startsWith', value: 'The' }] })).toBe('title:The*'); }); + it('quotes fulltext matches / notMatches (distinct from text contains)', () => { + expect(compile({ match: 'all', children: [{ field: 'plot', operator: 'matches', value: 'car chase' }] })).toBe('plot:"car chase"'); + expect(compile({ match: 'all', children: [{ field: 'plot', operator: 'notMatches', value: 'car' }] })).toBe('NOT plot:"car"'); + }); + it('emits numeric ranges', () => { expect(compile({ match: 'all', children: [{ field: 'minutes', operator: 'gt', value: '30' }] })).toBe('minutes:{30 TO *}'); expect(compile({ match: 'all', children: [{ field: 'minutes', operator: 'lt', value: '90' }] })).toBe('minutes:{* TO 90}'); diff --git a/web/src/builder/rules/compile.ts b/web/src/builder/rules/compile.ts index ac8f90700..7e3ad7fca 100644 --- a/web/src/builder/rules/compile.ts +++ b/web/src/builder/rules/compile.ts @@ -15,15 +15,15 @@ function compileRule(rule: Rule): string { const v = rule.value; switch (rule.operator) { case 'is': + case 'matches': // fulltext: same quoted form as text `is`, disambiguated by field type on parse return `${f}:${quote(v)}`; case 'isNot': + case 'notMatches': return `NOT ${f}:${quote(v)}`; case 'contains': return `${f}:*${escapeWild(v)}*`; case 'startsWith': return `${f}:${escapeWild(v)}*`; - case 'notContains': - return `NOT ${f}:${quote(v)}`; case 'eq': return `${f}:${v}`; case 'gt': diff --git a/web/src/builder/rules/types.ts b/web/src/builder/rules/types.ts index 6ea9dc1e3..3c5927b49 100644 --- a/web/src/builder/rules/types.ts +++ b/web/src/builder/rules/types.ts @@ -2,9 +2,10 @@ export type FieldType = 'text' | 'fulltext' | 'number' | 'date' | 'enum'; export type Match = 'all' | 'any'; export type Operator = - | 'is' | 'isNot' | 'contains' | 'startsWith' | 'notContains' - | 'eq' | 'gt' | 'lt' | 'between' - | 'before' | 'after'; + | 'is' | 'isNot' | 'contains' | 'startsWith' // text + | 'matches' | 'notMatches' // fulltext + | 'eq' | 'gt' | 'lt' | 'between' // number + | 'before' | 'after'; // date export interface Rule { field: string; @@ -25,7 +26,7 @@ export function isGroup(node: Rule | Group): node is Group { // Which fields each catalog type exposes as operators (used by the UI and tests). export const OPERATORS_BY_TYPE: Record = { text: ['is', 'isNot', 'contains', 'startsWith'], - fulltext: ['contains', 'notContains'], + fulltext: ['matches', 'notMatches'], enum: ['is', 'isNot'], number: ['eq', 'gt', 'lt', 'between'], date: ['before', 'after', 'between']