Skip to content

Commit 7a68802

Browse files
committed
fix(knowledge): validate tag-filter type against the slot, not the client claim
Greptile P1: operator validation trusted the client-supplied fieldType, so a numeric slot could be sent with fieldType 'text' + 'contains' and slip through to build a text LIKE on a numeric column. Validate against the slot's inherent type via getFieldTypeForSlot (the source of truth): reject unknown slots and fieldType/slot mismatches at the boundary before checking the operator.
1 parent 00d4872 commit 7a68802

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,23 @@ describe('parseDocumentTagFiltersParam', () => {
6060
)
6161
).toThrow()
6262
})
63+
64+
it('rejects a fieldType that does not match the tag slot', () => {
65+
// number1 is a numeric column; claiming it is text must fail
66+
expect(() =>
67+
parseDocumentTagFiltersParam(
68+
JSON.stringify([
69+
{ tagSlot: 'number1', fieldType: 'text', operator: 'contains', value: 'x' },
70+
])
71+
)
72+
).toThrow()
73+
})
74+
75+
it('rejects an unknown tag slot', () => {
76+
expect(() =>
77+
parseDocumentTagFiltersParam(
78+
JSON.stringify([{ tagSlot: 'tag99', fieldType: 'text', operator: 'eq', value: 'x' }])
79+
)
80+
).toThrow()
81+
})
6382
})

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
wireDateSchema,
1414
} from '@/lib/api/contracts/knowledge/shared'
1515
import { defineRouteContract } from '@/lib/api/contracts/types'
16+
import { getFieldTypeForSlot } from '@/lib/knowledge/constants'
1617
import { getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
1718

1819
export const documentTagFilterSchema = z
@@ -24,8 +25,28 @@ export const documentTagFilterSchema = z
2425
valueTo: z.unknown().optional(),
2526
})
2627
.superRefine((filter, ctx) => {
27-
// Reject operators that aren't valid for the field type so a bad operator
28-
// returns a 400 instead of being silently dropped by the query builder.
28+
// The tag slot determines the column type, so validate against the slot
29+
// (the source of truth) — not just the client-supplied fieldType. Rejecting
30+
// unknown slots, type mismatches, and bad operators at the boundary returns
31+
// a 400 instead of the query builder silently dropping or mis-handling the
32+
// filter (e.g. a text `contains` aimed at a numeric column).
33+
const slotFieldType = getFieldTypeForSlot(filter.tagSlot)
34+
if (slotFieldType === null) {
35+
ctx.addIssue({
36+
code: 'custom',
37+
path: ['tagSlot'],
38+
message: `Unknown tag slot "${filter.tagSlot}"`,
39+
})
40+
return
41+
}
42+
if (slotFieldType !== filter.fieldType) {
43+
ctx.addIssue({
44+
code: 'custom',
45+
path: ['fieldType'],
46+
message: `fieldType "${filter.fieldType}" does not match tag slot "${filter.tagSlot}" (expected "${slotFieldType}")`,
47+
})
48+
return
49+
}
2950
const validOperators = getOperatorsForFieldType(filter.fieldType).map((op) => op.value)
3051
if (!validOperators.includes(filter.operator)) {
3152
ctx.addIssue({

0 commit comments

Comments
 (0)