fix: support relationship equality filters
This commit is contained in:
@@ -181,6 +181,53 @@ describe('evaluateView', () => {
|
||||
expect(result.map((e) => e.title)).toEqual(['A'])
|
||||
})
|
||||
|
||||
it('equals on relationship matches a single-item array by stem', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'session-trail', icon: null, color: null, sort: null,
|
||||
filters: {
|
||||
any: [
|
||||
{ field: 'belongs_to', op: 'equals', value: 'svc-session-trail' },
|
||||
{ field: 'related_to', op: 'equals', value: 'svc-session-trail' },
|
||||
],
|
||||
},
|
||||
}
|
||||
const entries = [
|
||||
makeEntry({ title: 'Matches', relationships: { related_to: ['[[svc-session-trail]]'] } }),
|
||||
makeEntry({ title: 'Bracketed value also matches', relationships: { related_to: ['[[svc-session-trail|Trail]]'] } }),
|
||||
makeEntry({ title: 'Other relation', relationships: { related_to: ['[[unrelated]]'] } }),
|
||||
makeEntry({ title: 'No rels', relationships: {} }),
|
||||
]
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result.map((e) => e.title)).toEqual(['Matches', 'Bracketed value also matches'])
|
||||
})
|
||||
|
||||
it('equals on relationship requires a single-item array (mirrors Rust semantics)', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'single', icon: null, color: null, sort: null,
|
||||
filters: { all: [{ field: 'related_to', op: 'equals', value: 'svc-session-trail' }] },
|
||||
}
|
||||
const entries = [
|
||||
makeEntry({ title: 'Single', relationships: { related_to: ['[[svc-session-trail]]'] } }),
|
||||
makeEntry({ title: 'Multiple', relationships: { related_to: ['[[svc-session-trail]]', '[[other]]'] } }),
|
||||
]
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result.map((e) => e.title)).toEqual(['Single'])
|
||||
})
|
||||
|
||||
it('not_equals on relationship is the inverse of equals', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'not-equals', icon: null, color: null, sort: null,
|
||||
filters: { all: [{ field: 'related_to', op: 'not_equals', value: 'svc-session-trail' }] },
|
||||
}
|
||||
const entries = [
|
||||
makeEntry({ title: 'Single match', relationships: { related_to: ['[[svc-session-trail]]'] } }),
|
||||
makeEntry({ title: 'Multiple', relationships: { related_to: ['[[svc-session-trail]]', '[[other]]'] } }),
|
||||
makeEntry({ title: 'Other', relationships: { related_to: ['[[unrelated]]'] } }),
|
||||
]
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result.map((e) => e.title)).toEqual(['Multiple', 'Other'])
|
||||
})
|
||||
|
||||
it('any_of / none_of on relationship always use exact stem match', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'Exact list', icon: null, color: null, sort: null,
|
||||
|
||||
@@ -143,19 +143,34 @@ function hasArrayMatch(values: string[], condVal: string): boolean {
|
||||
))
|
||||
}
|
||||
|
||||
function isSingleRelationshipMatch(values: string[], condVal: string): boolean {
|
||||
return values.length === 1 && wikilinkEquals(values[0], condVal)
|
||||
}
|
||||
|
||||
const ARRAY_MATCHERS = {
|
||||
contains: hasArrayMatch,
|
||||
not_contains: hasArrayMatch,
|
||||
equals: isSingleRelationshipMatch,
|
||||
not_equals: isSingleRelationshipMatch,
|
||||
} satisfies Partial<Record<FilterCondition['op'], (values: string[], condVal: string) => boolean>>
|
||||
|
||||
const NEGATED_ARRAY_OPS = new Set<FilterCondition['op']>(['not_contains', 'not_equals', 'none_of'])
|
||||
const ARRAY_SET_OPS = new Set<FilterCondition['op']>(['any_of', 'none_of'])
|
||||
|
||||
function evaluateArrayCondition(cond: FilterCondition, values: string[], condVal: string, regex: RegExp | null): boolean {
|
||||
const { op, value } = cond
|
||||
if (regex) return evaluateRegexArrayCondition(op, values, regex)
|
||||
|
||||
if (op === 'contains') return hasArrayMatch(values, condVal)
|
||||
if (op === 'not_contains') return !hasArrayMatch(values, condVal)
|
||||
if (op === 'any_of' && Array.isArray(value)) {
|
||||
return values.some((item) => (value as string[]).some((v) => wikilinkEquals(item, v)))
|
||||
const matcher = ARRAY_MATCHERS[op as keyof typeof ARRAY_MATCHERS]
|
||||
if (matcher) {
|
||||
const matched = matcher(values, condVal)
|
||||
return NEGATED_ARRAY_OPS.has(op) ? !matched : matched
|
||||
}
|
||||
if (op === 'none_of' && Array.isArray(value)) {
|
||||
return !values.some((item) => (value as string[]).some((v) => wikilinkEquals(item, v)))
|
||||
}
|
||||
return false
|
||||
if (!ARRAY_SET_OPS.has(op)) return false
|
||||
if (!Array.isArray(value)) return false
|
||||
|
||||
const matched = values.some((item) => (value as string[]).some((v) => wikilinkEquals(item, v)))
|
||||
return NEGATED_ARRAY_OPS.has(op) ? !matched : matched
|
||||
}
|
||||
|
||||
function evaluateRegexScalarCondition(op: FilterCondition['op'], fieldRaw: string, regex: RegExp): boolean {
|
||||
|
||||
Reference in New Issue
Block a user