Skip to content

Commit b9f2b29

Browse files
AI Assistant: validate filter field existence for filter command (DevExpress#33830)
1 parent 5951a31 commit b9f2b29

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,38 @@ describe('filterValueCommand', () => {
436436
expect(result.status).toBe('failure');
437437
});
438438

439+
it('returns failure when a field has no corresponding column', async () => {
440+
const instance = await createGrid();
441+
const spy = jest.spyOn(instance, 'option');
442+
const callbacks = createCallbacks();
443+
444+
const result = await filterValueCommand.execute(instance, callbacks)({
445+
expression: singleBasic('nonexistent', '=', 'Alpha'),
446+
});
447+
448+
expect(spy).not.toHaveBeenCalled();
449+
expect(result.status).toBe('failure');
450+
});
451+
452+
it('succeeds when a field maps to a hidden but existing column', async () => {
453+
const instance = await createGrid({
454+
columns: [
455+
{ dataField: 'id', dataType: 'number' },
456+
{ dataField: 'name', dataType: 'string' },
457+
{ dataField: 'age', dataType: 'number', visible: false },
458+
],
459+
});
460+
const spy = jest.spyOn(instance, 'option');
461+
const callbacks = createCallbacks();
462+
463+
const result = await filterValueCommand.execute(instance, callbacks)({
464+
expression: singleBasic('age', '>', 10),
465+
});
466+
467+
expect(spy).toHaveBeenCalledWith('filterValue', ['age', '>', 10]);
468+
expect(result.status).toBe('success');
469+
});
470+
439471
it('tolerates unreachable extra nodes', async () => {
440472
const instance = await createGrid();
441473
const spy = jest.spyOn(instance, 'option');

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,15 @@ function convertFilterExprToArray(
9494
const { expr } = node;
9595
switch (expr.type) {
9696
case 'basic': {
97-
const dataType = component.columnOption(expr.field, 'dataType');
98-
const resolved = resolveFilterValue(dataType, expr.value);
97+
const column = component.columnOption(expr.field);
98+
99+
if (!isDefined(column)) {
100+
// Filter sync requires an existing column for every field in a basic expression
101+
throw new Error(`Unknown column: ${expr.field}`);
102+
}
103+
104+
const resolved = resolveFilterValue(column.dataType, expr.value);
105+
99106
return [expr.field, expr.operator, resolved];
100107
}
101108
case 'combined':
@@ -131,7 +138,7 @@ const getFilterSuccessMessage = async (
131138

132139
export const filterValueCommand = defineGridCommand({
133140
name: 'filterValue',
134-
description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":<unique string like "n1">,"expr":<expression>}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date or datetime, always use "YYYY-MM-DDTHH:mm:ss" format without timezone suffix, e.g. "2024-05-10T00:00:00" for midnight or "2024-05-10T14:30:00" for a specific time. Always include the "T" and time part. Do NOT use date-only format like "2024-05-10" without time. Do NOT append "Z" or any timezone offset unless the user explicitly requests it. Do NOT use natural language for dates. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.',
141+
description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":<unique string like "n1">,"expr":<expression>}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" of every basic expression MUST be the dataField of a column that exists in the grid (not the caption); the column may be hidden, but it must exist. Never filter on a field that has no corresponding column. Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date or datetime, always use "YYYY-MM-DDTHH:mm:ss" format without timezone suffix, e.g. "2024-05-10T00:00:00" for midnight or "2024-05-10T14:30:00" for a specific time. Always include the "T" and time part. Do NOT use date-only format like "2024-05-10" without time. Do NOT append "Z" or any timezone offset unless the user explicitly requests it. Do NOT use natural language for dates. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.',
135142
schema: filterValueCommandSchema,
136143
execute: (component, { success, failure }) => async (args): Promise<CommandResult> => {
137144
let defaultMessage = args.expression === null

0 commit comments

Comments
 (0)