Skip to content

Commit f44afb5

Browse files
committed
fix(knowledge): validate tag-filter values against the field type
Greptile P1: value/valueTo were z.unknown(), so a number filter accepted 'abc', a date filter 'not-a-date', etc. — unusable values the query builder then silently dropped. Add a shared isValidFilterValue (single source of truth in filters/types) and reject unusable value/valueTo at the boundary, including the between upper bound.
1 parent 7a68802 commit f44afb5

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

apps/sim/lib/api/contracts/knowledge/documents.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,59 @@ describe('parseDocumentTagFiltersParam', () => {
7979
)
8080
).toThrow()
8181
})
82+
83+
it('rejects values that are unusable for the field type', () => {
84+
// non-numeric value on a number field
85+
expect(() =>
86+
parseDocumentTagFiltersParam(
87+
JSON.stringify([{ tagSlot: 'number1', fieldType: 'number', operator: 'eq', value: 'abc' }])
88+
)
89+
).toThrow()
90+
// non-date value on a date field
91+
expect(() =>
92+
parseDocumentTagFiltersParam(
93+
JSON.stringify([{ tagSlot: 'date1', fieldType: 'date', operator: 'eq', value: 'nope' }])
94+
)
95+
).toThrow()
96+
// non-boolean value on a boolean field
97+
expect(() =>
98+
parseDocumentTagFiltersParam(
99+
JSON.stringify([
100+
{ tagSlot: 'boolean1', fieldType: 'boolean', operator: 'eq', value: 'maybe' },
101+
])
102+
)
103+
).toThrow()
104+
})
105+
106+
it('rejects a between filter missing a usable upper bound', () => {
107+
expect(() =>
108+
parseDocumentTagFiltersParam(
109+
JSON.stringify([
110+
{
111+
tagSlot: 'number1',
112+
fieldType: 'number',
113+
operator: 'between',
114+
value: '1',
115+
valueTo: 'x',
116+
},
117+
])
118+
)
119+
).toThrow()
120+
})
121+
122+
it('accepts a valid number, date, boolean, and between filter', () => {
123+
const filters = [
124+
{ tagSlot: 'number1', fieldType: 'number', operator: 'gte', value: '42' },
125+
{ tagSlot: 'date1', fieldType: 'date', operator: 'eq', value: '2026-04-21' },
126+
{ tagSlot: 'boolean1', fieldType: 'boolean', operator: 'eq', value: 'true' },
127+
{
128+
tagSlot: 'number2',
129+
fieldType: 'number',
130+
operator: 'between',
131+
value: '1',
132+
valueTo: '10',
133+
},
134+
]
135+
expect(parseDocumentTagFiltersParam(JSON.stringify(filters))).toEqual(filters)
136+
})
82137
})

apps/sim/lib/api/contracts/knowledge/documents.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from '@/lib/api/contracts/knowledge/shared'
1515
import { defineRouteContract } from '@/lib/api/contracts/types'
1616
import { getFieldTypeForSlot } from '@/lib/knowledge/constants'
17-
import { getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
17+
import { getOperatorsForFieldType, isValidFilterValue } from '@/lib/knowledge/filters/types'
1818

1919
export const documentTagFilterSchema = z
2020
.object({
@@ -54,6 +54,23 @@ export const documentTagFilterSchema = z
5454
path: ['operator'],
5555
message: `Unsupported operator "${filter.operator}" for a ${filter.fieldType} tag filter`,
5656
})
57+
return
58+
}
59+
if (!isValidFilterValue(filter.fieldType, filter.value)) {
60+
ctx.addIssue({
61+
code: 'custom',
62+
path: ['value'],
63+
message: `Invalid value for a ${filter.fieldType} tag filter`,
64+
})
65+
}
66+
// `between` is only valid for number/date (enforced by the operator check
67+
// above), and needs a usable upper bound.
68+
if (filter.operator === 'between' && !isValidFilterValue(filter.fieldType, filter.valueTo)) {
69+
ctx.addIssue({
70+
code: 'custom',
71+
path: ['valueTo'],
72+
message: `Invalid second value for a ${filter.fieldType} "between" tag filter`,
73+
})
5774
}
5875
})
5976
export type DocumentTagFilter = z.output<typeof documentTagFilterSchema>

apps/sim/lib/knowledge/filters/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,32 @@ export function getOperatorsForFieldType(fieldType: FilterFieldType): OperatorIn
189189
return []
190190
}
191191
}
192+
193+
/** Wire format for a date filter value (`YYYY-MM-DD`). */
194+
const DATE_ONLY_VALUE = /^\d{4}-\d{2}-\d{2}$/
195+
196+
/**
197+
* Whether a raw filter value is usable for the given field type. Shared source
198+
* of truth so the API boundary can reject unusable values (e.g. `"abc"` for a
199+
* number, `"not-a-date"` for a date) instead of letting them be silently
200+
* dropped further down. Values arrive as strings from the filter UI.
201+
*/
202+
export function isValidFilterValue(fieldType: FilterFieldType, value: unknown): boolean {
203+
if (value === undefined || value === null) return false
204+
switch (fieldType) {
205+
case 'text':
206+
return typeof value === 'string' && value.length > 0
207+
case 'number':
208+
if (typeof value === 'number') return Number.isFinite(value)
209+
return typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))
210+
case 'date':
211+
return typeof value === 'string' && DATE_ONLY_VALUE.test(value)
212+
case 'boolean':
213+
return (
214+
typeof value === 'boolean' ||
215+
(typeof value === 'string' && ['true', 'false'].includes(value.trim().toLowerCase()))
216+
)
217+
default:
218+
return false
219+
}
220+
}

0 commit comments

Comments
 (0)