diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts index 6365e4c075ec..4dea8c208a54 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts @@ -23,6 +23,7 @@ export class ExecuteGridAssistantCommand extends BaseCommand< }; } + // TODO: check response more carefully protected parseResult( response: ExecuteGridAssistantCommandResponse, ): ExecuteGridAssistantCommandResult { diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts index 488743b57431..a01011aeb5c3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts @@ -16,6 +16,12 @@ import { AI_ASSISTANT_AUTHOR_ID, MessageStatus, } from '../const'; +import { GridCommands } from '../grid_commands'; +import type { CommandResult } from '../types'; + +jest.mock('../grid_commands'); + +const MockedGridCommands = GridCommands as jest.MockedClass; let sendRequestCallbacks: RequestCallbacks = {}; @@ -59,6 +65,15 @@ const getStore = (controller: AIAssistantController): ArrayStore { beforeEach(() => { jest.clearAllMocks(); + + // TODO: Rework the tests using updated GridCommands implementation + (MockedGridCommands.mockImplementation as jest.Mock).call( + MockedGridCommands, + () => ({ + validate: jest.fn().mockReturnValue(true), + executeCommands: jest.fn<() => Promise>().mockResolvedValue([{ status: 'success', message: 'sort' }]), + }), + ); }); describe('getMessageDataSource', () => { diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_integration_controller.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_integration_controller.integration.test.ts index e37dce86bf56..e15adc3d8abc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_integration_controller.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_integration_controller.integration.test.ts @@ -21,7 +21,7 @@ import { beforeTest, createDataGrid, } from '../../__tests__/__mock__/helpers/utils'; -import { AIAssistantIntegrationController } from '../m_ai_assistant_integration_controller'; +import { AIAssistantIntegrationController } from '../ai_assistant_integration_controller'; interface SendRequestResult { promise: Promise; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/grid_commands.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/grid_commands.test.ts new file mode 100644 index 000000000000..27dd9e257d70 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/grid_commands.test.ts @@ -0,0 +1,1081 @@ +import { + describe, expect, it, jest, +} from '@jest/globals'; +import { z } from 'zod'; + +import type { InternalGrid } from '../../m_types'; +import { GridCommands } from '../grid_commands'; +import type { + CommandCallbacks, + CommandResult, + CustomizeResponseText, + GridCommand, +} from '../types'; + +interface Branch { + type: string; + description: string; + required: string[]; + additionalProperties: boolean; + properties: { + name: { type: string; enum: string[] }; + args: Record; + }; +} + +interface SchemaShape { + $schema: string; + type: string; + required: string[]; + additionalProperties: boolean; + properties: { + actions: { + type: string; + description: string; + items: { anyOf: Branch[] }; + }; + }; +} + +const createMockComponent = (): InternalGrid => ({}) as InternalGrid; + +const createMockCommand = ( + name: string, + overrides: Partial = {}, +): GridCommand => ({ + name, + description: `Test command: ${name}`, + schema: z.object({}), + execute: ( + _component: InternalGrid, + { success }: CommandCallbacks, + ) => async (): Promise => success(), + ...overrides, +}); + +describe('GridCommands', () => { + describe('constructor', () => { + it('should accept an empty commands array', () => { + const component = createMockComponent(); + + expect(() => new GridCommands(component, [])).not.toThrow(); + }); + + it('should store component for use by executeCommands', async () => { + const component = createMockComponent(); + const executeSpy = jest.fn( + ( + _comp: InternalGrid, + { success }: CommandCallbacks, + ) => async (): Promise => success('done'), + ); + const command = createMockCommand('test', { execute: executeSpy }); + const gridCommands = new GridCommands(component, [command]); + + await gridCommands.executeCommands([{ name: 'test', args: {} }]); + + expect(executeSpy).toHaveBeenCalledWith( + component, + expect.objectContaining({ + success: expect.any(Function), + failure: expect.any(Function), + }), + ); + }); + + it('should store commands in an internal registry indexed by name', () => { + const component = createMockComponent(); + const commandA = createMockCommand('commandA'); + const commandB = createMockCommand('commandB'); + const gridCommands = new GridCommands( + component, + [commandA, commandB], + ); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const commandNames = schema.properties.actions.items.anyOf.map( + (branch) => branch.properties.name.enum[0], + ); + + expect(commandNames).toEqual(['commandA', 'commandB']); + }); + + it('should throw if duplicate command names are provided', () => { + const component = createMockComponent(); + const command1 = createMockCommand('duplicate'); + const command2 = createMockCommand('duplicate'); + + expect( + () => new GridCommands(component, [command1, command2]), + ).toThrow('Duplicate command name: "duplicate"'); + }); + }); + + describe('success helper', () => { + it('should return CommandResult with status success and default message when called without argument', async () => { + const component = createMockComponent(); + const command = createMockCommand('test', { + execute: (_comp, { success }) => async () => success(), + }); + const gridCommands = new GridCommands(component, [command]); + + const results = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + + expect(results[0].status).toBe('success'); + expect(typeof results[0].message).toBe('string'); + }); + + it('should return CommandResult with status success and custom message', async () => { + const component = createMockComponent(); + const command = createMockCommand('test', { + execute: (_comp, { success }) => async () => success('Custom msg'), + }); + const gridCommands = new GridCommands(component, [command]); + + const results = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + + expect(results[0]).toEqual({ + status: 'success', + message: 'Custom msg', + }); + }); + }); + + describe('failure helper', () => { + it('should return CommandResult with status failure and default message when called without argument', async () => { + const component = createMockComponent(); + const command = createMockCommand('test', { + execute: (_comp, { failure }) => async () => failure(), + }); + const gridCommands = new GridCommands(component, [command]); + + const results = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + + expect(results[0].status).toBe('failure'); + expect(typeof results[0].message).toBe('string'); + }); + + it('should return CommandResult with status failure and custom message', async () => { + const component = createMockComponent(); + const command = createMockCommand('test', { + execute: (_comp, { failure }) => async () => failure('Custom msg'), + }); + const gridCommands = new GridCommands(component, [command]); + + const results = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + + expect(results[0]).toEqual({ + status: 'failure', + message: 'Custom msg', + }); + }); + }); + + describe('buildResponseSchema', () => { + it('should return valid JSON Schema draft-07 object', () => { + const gridCommands = new GridCommands(createMockComponent(), []); + const schema = gridCommands.buildResponseSchema() as SchemaShape; + + expect(schema.$schema).toBe('http://json-schema.org/draft-07/schema#'); + expect(schema.type).toBe('object'); + expect(schema.required).toEqual(['actions']); + expect(schema.additionalProperties).toBe(false); + }); + + it('should have anyOf with one branch per registered command', () => { + const commandA = createMockCommand('commandA'); + const commandB = createMockCommand('commandB'); + const gridCommands = new GridCommands( + createMockComponent(), + [commandA, commandB], + ); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const { anyOf } = schema.properties.actions.items; + + expect(anyOf).toHaveLength(2); + }); + + it('should include description from GridCommand.description in each branch', () => { + const command = createMockCommand('sorting', { + description: 'Apply sorting to one or more columns', + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const branch = schema.properties.actions.items.anyOf[0]; + + expect(branch.description).toBe('Apply sorting to one or more columns'); + }); + + it('should have name.enum with exactly one command name in each branch', () => { + const command = createMockCommand('sorting'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const branch = schema.properties.actions.items.anyOf[0]; + + expect(branch.properties.name).toEqual({ + type: 'string', + enum: ['sorting'], + }); + }); + + it('should have args with command schema including additionalProperties false', () => { + const command = createMockCommand('test', { + schema: z.object({ + dataField: z.string(), + sortOrder: z.enum(['asc', 'desc']), + }), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const { args } = schema.properties.actions.items.anyOf[0].properties; + + expect(args.type).toBe('object'); + expect(args.additionalProperties).toBe(false); + expect(args.required).toEqual(['dataField', 'sortOrder']); + expect(args.properties).toBeDefined(); + }); + + it('should set additionalProperties false on every object level', () => { + const command = createMockCommand('test'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + + // Root level + expect(schema.additionalProperties).toBe(false); + // Branch level + const branch = schema.properties.actions.items.anyOf[0]; + expect(branch.additionalProperties).toBe(false); + // Args level + expect(branch.properties.args.additionalProperties).toBe(false); + }); + + it('should not have anyOf at root schema level', () => { + const command = createMockCommand('test'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as Record; + + expect(schema.anyOf).toBeUndefined(); + expect(schema.oneOf).toBeUndefined(); + expect(schema.allOf).toBeUndefined(); + }); + + it('should return empty anyOf with no commands registered', () => { + const gridCommands = new GridCommands(createMockComponent(), []); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const { anyOf } = schema.properties.actions.items; + + expect(anyOf).toEqual([]); + }); + + it('should produce empty args schema for no-arg commands', () => { + const command = createMockCommand('clearSorting'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const { args } = schema.properties.actions.items.anyOf[0].properties; + + expect(args.type).toBe('object'); + expect(args.additionalProperties).toBe(false); + expect(args.properties).toEqual({}); + }); + + it('should produce different schemas for different command registries', () => { + const gc1 = new GridCommands(createMockComponent(), [ + createMockCommand('commandA'), + ]); + const gc2 = new GridCommands(createMockComponent(), [ + createMockCommand('commandB'), + ]); + + const schema1 = gc1.buildResponseSchema() as SchemaShape; + const schema2 = gc2.buildResponseSchema() as SchemaShape; + + const names1 = schema1.properties.actions.items.anyOf.map( + (b) => b.properties.name.enum[0], + ); + const names2 = schema2.properties.actions.items.anyOf.map( + (b) => b.properties.name.enum[0], + ); + + expect(names1).toEqual(['commandA']); + expect(names2).toEqual(['commandB']); + }); + + it('should produce correct required array for commands with required fields', () => { + const command = createMockCommand('test', { + schema: z.object({ + field1: z.string(), + field2: z.number().optional(), + }), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const schema = gridCommands.buildResponseSchema() as SchemaShape; + const { args } = schema.properties.actions.items.anyOf[0].properties; + + expect(args.required).toEqual(['field1']); + }); + }); + + describe('validateResponse', () => { + it('should return true for valid response with known command names and correct args', () => { + const command = createMockCommand('test', { + schema: z.object({ value: z.string() }), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [{ name: 'test', args: { value: 'hello' } }], + ); + + expect(result).toBe(true); + }); + + it('should return false if any action has an unknown name', () => { + const command = createMockCommand('known'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [{ name: 'unknown', args: {} }], + ); + + expect(result).toBe(false); + }); + + it('should return false if any action name is not a string', () => { + const gridCommands = new GridCommands(createMockComponent(), [ + createMockCommand('test'), + ]); + + expect(gridCommands.validate( + [{ name: 123 as unknown as string, args: {} }], + )).toBe(false); + + expect(gridCommands.validate( + [{ name: true as unknown as string, args: {} }], + )).toBe(false); + }); + + it('should return false if any action name is an empty string', () => { + const gridCommands = new GridCommands(createMockComponent(), [ + createMockCommand('test'), + ]); + + const result = gridCommands.validate( + [{ name: '', args: {} }], + ); + + expect(result).toBe(false); + }); + + it('should return false if any action is missing name or args', () => { + const gridCommands = new GridCommands(createMockComponent(), [ + createMockCommand('test'), + ]); + + expect(gridCommands.validate( + [{ args: {} } as any], + )).toBe(false); + + expect(gridCommands.validate( + [{ name: 'test' } as any], + )).toBe(false); + }); + + it('should return false if any action args is null', () => { + const gridCommands = new GridCommands(createMockComponent(), [ + createMockCommand('test'), + ]); + + const result = gridCommands.validate( + [{ name: 'test', args: null as unknown as Record }], + ); + + expect(result).toBe(false); + }); + + it('should return false if any action args has wrong types for required properties', () => { + const command = createMockCommand('test', { + schema: z.object({ value: z.string() }), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [{ name: 'test', args: { value: 123 } }], + ); + + expect(result).toBe(false); + }); + + it('should return false if any action args is missing required properties', () => { + const command = createMockCommand('test', { + schema: z.object({ value: z.string() }), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [{ name: 'test', args: {} }], + ); + + expect(result).toBe(false); + }); + + it('should return false if any action args contains extra properties', () => { + const command = createMockCommand('test', { + schema: z.object({ value: z.string() }), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [{ name: 'test', args: { value: 'ok', extra: true } }], + ); + + expect(result).toBe(false); + }); + + it('should return true for an empty actions array', () => { + const gridCommands = new GridCommands(createMockComponent(), []); + + const result = gridCommands.validate([]); + + expect(result).toBe(true); + }); + + it('should return true for no-arg commands when args is empty object', () => { + const command = createMockCommand('clearFilter'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [{ name: 'clearFilter', args: {} }], + ); + + expect(result).toBe(true); + }); + + it('should reject entire response on first mismatch', () => { + const command = createMockCommand('valid'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const result = gridCommands.validate( + [ + { name: 'valid', args: {} }, + { name: 'invalid', args: {} }, + ], + ); + + expect(result).toBe(false); + }); + }); + + describe('executeCommands', () => { + it('should return empty array for empty commands', async () => { + const gridCommands = new GridCommands(createMockComponent(), []); + + const results = await gridCommands.executeCommands([]); + + expect(results).toEqual([]); + }); + + it('should execute commands in the order provided', async () => { + const executionOrder: string[] = []; + + const commandA = createMockCommand('a', { + execute: (_comp, { success }) => async () => { + executionOrder.push('a'); + return success(); + }, + }); + const commandB = createMockCommand('b', { + execute: (_comp, { success }) => async () => { + executionOrder.push('b'); + return success(); + }, + }); + const commandC = createMockCommand('c', { + execute: (_comp, { success }) => async () => { + executionOrder.push('c'); + return success(); + }, + }); + const gridCommands = new GridCommands( + createMockComponent(), + [commandA, commandB, commandC], + ); + + await gridCommands.executeCommands([ + { name: 'a', args: {} }, + { name: 'b', args: {} }, + { name: 'c', args: {} }, + ]); + + expect(executionOrder).toEqual(['a', 'b', 'c']); + }); + + it('should await each command before starting the next', async () => { + const executionOrder: string[] = []; + + const commandA = createMockCommand('a', { + execute: (_comp, { success }) => async () => { + await new Promise((resolve) => { setTimeout(resolve, 50); }); + executionOrder.push('a'); + return success(); + }, + }); + const commandB = createMockCommand('b', { + execute: (_comp, { success }) => async () => { + executionOrder.push('b'); + return success(); + }, + }); + const gridCommands = new GridCommands( + createMockComponent(), + [commandA, commandB], + ); + + await gridCommands.executeCommands([ + { name: 'a', args: {} }, + { name: 'b', args: {} }, + ]); + + expect(executionOrder).toEqual(['a', 'b']); + }); + + it('should return one CommandResult per executed command', async () => { + const commandA = createMockCommand('a'); + const commandB = createMockCommand('b'); + const gridCommands = new GridCommands( + createMockComponent(), + [commandA, commandB], + ); + + const results = await gridCommands.executeCommands([ + { name: 'a', args: {} }, + { name: 'b', args: {} }, + ]); + + expect(results).toHaveLength(2); + expect(results[0].status).toBe('success'); + expect(results[1].status).toBe('success'); + }); + + it('should produce failure result when executor throws synchronously', async () => { + const command = createMockCommand('throwing', { + execute: () => () => { + throw new Error('sync error'); + }, + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const results = await gridCommands.executeCommands([ + { name: 'throwing', args: {} }, + ]); + + expect(results[0].status).toBe('failure'); + }); + + it('should produce failure result when async executor rejects', async () => { + const command = createMockCommand('rejecting', { + execute: () => async () => { + throw new Error('async error'); + }, + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const results = await gridCommands.executeCommands([ + { name: 'rejecting', args: {} }, + ]); + + expect(results[0].status).toBe('failure'); + }); + + it('should throw for unknown command name', async () => { + const gridCommands = new GridCommands(createMockComponent(), [ + createMockCommand('known'), + ]); + + await expect( + gridCommands.executeCommands([{ name: 'unknown', args: {} }]), + ).rejects.toThrow('Unknown command: unknown'); + }); + + it('should reset _executing after unknown command throw so subsequent calls work', async () => { + const command = createMockCommand('known'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + await expect( + gridCommands.executeCommands([{ name: 'unknown', args: {} }]), + ).rejects.toThrow(); + + const results = await gridCommands.executeCommands([ + { name: 'known', args: {} }, + ]); + + expect(results[0].status).toBe('success'); + }); + + it('should throw if called while another executeCommands is in progress', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const blockingCommand = createMockCommand('blocking', { + execute: (_comp, { success }) => async () => { + await expect( + gridCommands.executeCommands([]), + ).rejects.toThrow('executeCommands is already in progress'); + return success(); + }, + }); + + gridCommands = new GridCommands(component, [blockingCommand]); + + await gridCommands.executeCommands([ + { name: 'blocking', args: {} }, + ]); + }); + + it('should allow subsequent calls after first call completes', async () => { + const command = createMockCommand('test'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const results1 = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + const results2 = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + + expect(results1[0].status).toBe('success'); + expect(results2[0].status).toBe('success'); + }); + + it('should record success and failure statuses correctly', async () => { + const successCommand = createMockCommand('ok', { + execute: (_comp, { success }) => async () => success(), + }); + const failCommand = createMockCommand('fail', { + execute: (_comp, { failure }) => async () => failure(), + }); + const gridCommands = new GridCommands( + createMockComponent(), + [successCommand, failCommand], + ); + + const results = await gridCommands.executeCommands([ + { name: 'ok', args: {} }, + { name: 'fail', args: {} }, + ]); + + expect(results[0].status).toBe('success'); + expect(results[1].status).toBe('failure'); + }); + }); + + describe('abort', () => { + it('should stop execution mid-way and return partial results plus one aborted entry', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const first = createMockCommand('first', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + return success('first done'); + }, + }); + const second = createMockCommand('second'); + const third = createMockCommand('third'); + + gridCommands = new GridCommands(component, [first, second, third]); + + const results = await gridCommands.executeCommands([ + { name: 'first', args: {} }, + { name: 'second', args: {} }, + { name: 'third', args: {} }, + ]); + + expect(results).toHaveLength(2); + expect(results[0]).toEqual({ status: 'success', message: 'first done' }); + expect(results[1].status).toBe('aborted'); + }); + + it('should be idempotent - calling multiple times has no additional effect', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const first = createMockCommand('first', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + gridCommands.abort(); + gridCommands.abort(); + return success(); + }, + }); + const second = createMockCommand('second'); + + gridCommands = new GridCommands(component, [first, second]); + + const results = await gridCommands.executeCommands([ + { name: 'first', args: {} }, + { name: 'second', args: {} }, + ]); + + expect(results).toHaveLength(2); + expect(results[0].status).toBe('success'); + expect(results[1].status).toBe('aborted'); + }); + + it('should only add one aborted entry for the first skipped command', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const first = createMockCommand('first', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + return success(); + }, + }); + const second = createMockCommand('second'); + const third = createMockCommand('third'); + const fourth = createMockCommand('fourth'); + + gridCommands = new GridCommands( + component, + [first, second, third, fourth], + ); + + const results = await gridCommands.executeCommands([ + { name: 'first', args: {} }, + { name: 'second', args: {} }, + { name: 'third', args: {} }, + { name: 'fourth', args: {} }, + ]); + + expect(results).toHaveLength(2); + expect(results[1].status).toBe('aborted'); + }); + + it('should reset _aborted on next successful executeCommands start', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const abortSimulation = createMockCommand('abort', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + return success(); + }, + }); + const normal = createMockCommand('normal'); + + gridCommands = new GridCommands(component, [abortSimulation, normal]); + + // First call: abort triggered during execution + const results1 = await gridCommands.executeCommands([ + { name: 'abort', args: {} }, + { name: 'normal', args: {} }, + ]); + expect(results1[1].status).toBe('aborted'); + + // Second call: _aborted was reset, runs normally + const results2 = await gridCommands.executeCommands([ + { name: 'normal', args: {} }, + ]); + expect(results2[0].status).toBe('success'); + }); + + it('should not reset _aborted when concurrent call is rejected by reentrancy guard', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const blockingCommand = createMockCommand('blocking', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + try { + await gridCommands.executeCommands([]); + } catch { + // Expected: reentrancy rejection + } + return success(); + }, + }); + const nextCommand = createMockCommand('next'); + + gridCommands = new GridCommands(component, [blockingCommand, nextCommand]); + + const results = await gridCommands.executeCommands([ + { name: 'blocking', args: {} }, + { name: 'next', args: {} }, + ]); + + expect(results).toHaveLength(2); + expect(results[0].status).toBe('success'); + expect(results[1].status).toBe('aborted'); + }); + }); + + describe('isExecuting', () => { + it('should return false before executeCommands is called', () => { + const gridCommands = new GridCommands(createMockComponent(), []); + + expect(gridCommands.isExecuting()).toBe(false); + }); + + it('should return false after executeCommands completes', async () => { + const gridCommands = new GridCommands(createMockComponent(), []); + + await gridCommands.executeCommands([]); + + expect(gridCommands.isExecuting()).toBe(false); + }); + + it('should return true while executeCommands is in progress', async () => { + const component = createMockComponent(); + let capturedIsExecuting = false; + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const executeSpy = jest.fn(( + _comp: InternalGrid, + { success }: CommandCallbacks, + ) => async (): Promise => { + capturedIsExecuting = gridCommands.isExecuting(); + return success(); + }); + const spyCommand = createMockCommand('spy', { + execute: executeSpy, + }); + gridCommands = new GridCommands(component, [spyCommand]); + + await gridCommands.executeCommands([ + { name: 'spy', args: {} }, + ]); + + expect(capturedIsExecuting).toBe(true); + }); + + it('should return false after abort-induced exit', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const abortSimulation = createMockCommand('abort', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + return success(); + }, + }); + const next = createMockCommand('next'); + + gridCommands = new GridCommands(component, [abortSimulation, next]); + + await gridCommands.executeCommands([ + { name: 'abort', args: {} }, + { name: 'next', args: {} }, + ]); + + expect(gridCommands.isExecuting()).toBe(false); + }); + + it('should not change isExecuting state when concurrent call is rejected', async () => { + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + let isExecutingAfterRejection = true; + + const blockingCommand = createMockCommand('blocking', { + execute: (_comp, { success }) => async () => { + try { + await gridCommands.executeCommands([]); + } catch { + isExecutingAfterRejection = gridCommands.isExecuting(); + } + return success(); + }, + }); + + gridCommands = new GridCommands(component, [blockingCommand]); + + await gridCommands.executeCommands([ + { name: 'blocking', args: {} }, + ]); + + // isExecuting should still be true during the outer call + expect(isExecutingAfterRejection).toBe(true); + // After completion, it's false + expect(gridCommands.isExecuting()).toBe(false); + }); + }); + + describe('customizeResponseText', () => { + it('should use default messages when customizeResponseText is not provided', async () => { + const command = createMockCommand('test', { + execute: (_comp, { success }) => async () => success('default msg'), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const results = await gridCommands.executeCommands([ + { name: 'test', args: {} }, + ]); + + expect(results[0].message).toBe('default msg'); + }); + + it('should call customizeResponseText once per executed command with correct args', async () => { + const customizeSpy = jest.fn(() => undefined); + const command = createMockCommand('test'); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + await gridCommands.executeCommands( + [ + { name: 'test', args: { key: 'val1' } }, + { name: 'test', args: { key: 'val2' } }, + ], + customizeSpy, + ); + + expect(customizeSpy).toHaveBeenCalledTimes(2); + expect(customizeSpy).toHaveBeenNthCalledWith(1, 'test', { key: 'val1' }); + expect(customizeSpy).toHaveBeenNthCalledWith(2, 'test', { key: 'val2' }); + }); + + it('should replace both messages when returning { success, failure }', async () => { + const customizeResponseText: CustomizeResponseText = () => ({ + success: 'Custom success', + failure: 'Custom failure', + }); + + const successCommand = createMockCommand('ok', { + execute: (_comp, { success }) => async () => success('default'), + }); + const failCommand = createMockCommand('fail', { + execute: (_comp, { failure }) => async () => failure('default'), + }); + const gridCommands = new GridCommands( + createMockComponent(), + [successCommand, failCommand], + ); + + const results = await gridCommands.executeCommands( + [ + { name: 'ok', args: {} }, + { name: 'fail', args: {} }, + ], + customizeResponseText, + ); + + expect(results[0].message).toBe('Custom success'); + expect(results[1].message).toBe('Custom failure'); + }); + + it('should only replace success message when returning { success } and keep default failure', async () => { + const customizeResponseText: CustomizeResponseText = () => ({ + success: 'Custom success', + }); + + const failCommand = createMockCommand('fail', { + execute: (_comp, { failure }) => async () => failure('default failure'), + }); + const gridCommands = new GridCommands(createMockComponent(), [failCommand]); + + const results = await gridCommands.executeCommands( + [{ name: 'fail', args: {} }], + customizeResponseText, + ); + + expect(results[0].message).toBe('default failure'); + }); + + it('should only replace failure message when returning { failure } and keep default success', async () => { + const customizeResponseText: CustomizeResponseText = () => ({ + failure: 'Custom failure', + }); + + const successCommand = createMockCommand('ok', { + execute: (_comp, { success }) => async () => success('default success'), + }); + const gridCommands = new GridCommands(createMockComponent(), [successCommand]); + + const results = await gridCommands.executeCommands( + [{ name: 'ok', args: {} }], + customizeResponseText, + ); + + expect(results[0].message).toBe('default success'); + }); + + it('should leave default message when customizeResponseText returns undefined', async () => { + const customizeResponseText: CustomizeResponseText = () => undefined; + + const command = createMockCommand('test', { + execute: (_comp, { success }) => async () => success('original'), + }); + const gridCommands = new GridCommands(createMockComponent(), [command]); + + const results = await gridCommands.executeCommands( + [{ name: 'test', args: {} }], + customizeResponseText, + ); + + expect(results[0].message).toBe('original'); + }); + + it('should not call customizeResponseText for aborted entry', async () => { + const customizeSpy = jest.fn(() => ({ + success: 'custom', + })); + const component = createMockComponent(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let gridCommands: GridCommands; + + const abortSimulation = createMockCommand('abort', { + execute: (_comp, { success }) => async () => { + gridCommands.abort(); + return success(); + }, + }); + const skipped = createMockCommand('skipped'); + + gridCommands = new GridCommands(component, [abortSimulation, skipped]); + + const results = await gridCommands.executeCommands( + [ + { name: 'abort', args: {} }, + { name: 'skipped', args: {} }, + ], + customizeSpy, + ); + + expect(customizeSpy).toHaveBeenCalledTimes(1); + expect(customizeSpy).toHaveBeenCalledWith('abort', {}); + expect(results[1].status).toBe('aborted'); + }); + + it('should not call customizeResponseText when no commands are executed', async () => { + const customizeSpy = jest.fn(() => undefined); + const gridCommands = new GridCommands(createMockComponent(), []); + + await gridCommands.executeCommands([], customizeSpy); + + expect(customizeSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts index b29ff39703bc..e5c31a07ce9c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts @@ -8,9 +8,9 @@ import { fromPromise } from '@ts/core/utils/m_deferred'; import { hasCommandErrors } from '../ai_chat/utils'; import { Controller } from '../m_modules'; +import { AIAssistantIntegrationController } from './ai_assistant_integration_controller'; import { AI_ASSISTANT_AUTHOR, AI_ASSISTANT_AUTHOR_ID, MessageStatus } from './const'; import { GridCommands } from './grid_commands'; -import { AIAssistantIntegrationController } from './m_ai_assistant_integration_controller'; import type { CommandResults, } from './types'; @@ -33,7 +33,7 @@ export class AIAssistantController extends Controller { } private processResponse(response: ExecuteGridAssistantCommandResult): Promise { - if (!response?.actions) { + if (!response?.actions || !Array.isArray(response.actions)) { // TODO: need to localize default error message when there are no commands return Promise.reject(new Error('Default error message')); } @@ -87,7 +87,7 @@ export class AIAssistantController extends Controller { } public init(): void { - this.gridCommands = new GridCommands(this.component); + this.gridCommands = new GridCommands(this.component, []); this.messageStore = new ArrayStore({ key: 'id', }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_integration_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_integration_controller.ts similarity index 100% rename from packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_integration_controller.ts rename to packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_integration_controller.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts index 87dd1970b5b0..75a74313ae54 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts @@ -1,23 +1,202 @@ import type { ExecuteGridAssistantAction } from '@js/common/ai-integration'; +import messageLocalization from '@js/common/core/localization/message'; +import { isDefined, isObject } from '@js/core/utils/type'; +import { zodToJsonSchema } from 'zod-to-json-schema'; import type { InternalGrid } from '../m_types'; -import type { CommandResults } from './types'; +import type { + CommandCallbacks, + CommandResult, + CustomizeResponseText, + GridCommand, + JsonSchema, +} from './types'; + +const DEFAULT_SUCCESS_MESSAGE = 'dxDataGrid-aiAssistantSuccessMessage'; +const DEFAULT_FAILURE_MESSAGE = 'dxDataGrid-aiAssistantErrorMessage'; +const EXECUTION_ABORT_MESSAGE = 'dxDataGrid-aiAssistantExecutionAbortMessage'; export class GridCommands { - constructor(private readonly gridInstance: InternalGrid) { + private readonly component: InternalGrid; + + private readonly commands: Map>>; + + private _executing = false; + + private _aborted = false; + + constructor(component: InternalGrid, commands: GridCommand[]) { + this.component = component; + this.commands = new Map(); + + for (const command of commands) { + if (this.commands.has(command.name)) { + throw new Error(`Duplicate command name: "${command.name}"`); + } + this.commands.set(command.name, command); + } + } + + private static success(message?: string): CommandResult { + return { + status: 'success', + message: message ?? messageLocalization.format(DEFAULT_SUCCESS_MESSAGE), + }; + } + + private static failure(message?: string): CommandResult { + return { + status: 'failure', + message: message ?? messageLocalization.format(DEFAULT_FAILURE_MESSAGE), + }; + } + + private static applyCustomizedResponseText( + result: CommandResult, + name: string, + args: Record, + customizeResponseText?: CustomizeResponseText, + ): void { + const customMessages = customizeResponseText?.(name, args); + const customMessage = customMessages?.[result.status]; + + if (isDefined(customMessage)) { + result.message = customMessage; + } + } + + public abort(): void { + this._aborted = true; + } + + public isAborted(): boolean { + return this._aborted; + } + + public isExecuting(): boolean { + return this._executing; + } + + public buildResponseSchema(): JsonSchema { + const branches = [...this.commands.values()].map((command) => { + const argsSchema = zodToJsonSchema(command.schema, { target: 'jsonSchema7' }); + + // Remove $schema from nested schemas since it's only necessary at root + delete argsSchema.$schema; + + return { + type: 'object', + description: command.description, + required: ['name', 'args'], + additionalProperties: false, + properties: { + name: { + type: 'string', + enum: [command.name], + }, + args: argsSchema, + }, + }; + }); + + return { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + required: ['actions'], + additionalProperties: false, + properties: { + actions: { + type: 'array', + description: 'The list of grid commands and corresponding arguments to execute', + items: { + anyOf: branches, + }, + }, + }, + }; } - // TODO: need to implement real validation logic - // eslint-disable-next-line @typescript-eslint/no-unused-vars public validate(actions: ExecuteGridAssistantAction[]): boolean { + for (const action of actions as Record[]) { + if (!action || typeof action.name !== 'string' || action.name === '') { + return false; + } + + const command = this.commands.get(action.name); + + if (!command) { + return false; + } + + if (!isDefined(action.args) || !isObject(action.args)) { + return false; + } + + const parseResult = command.schema.strict().safeParse(action.args); + + if (!parseResult.success) { + return false; + } + } + return true; } - // TODO: need to implement real command execution logic - public executeCommands(actions: ExecuteGridAssistantAction[]): Promise { - return Promise.resolve(actions.map((action) => ({ - status: action.name.includes('Error') ? 'failure' : 'success', - message: action.name, - }))); + private async executeCommand( + command: GridCommand>, + args: Record, + callbacks: CommandCallbacks, + ): Promise { + try { + const executor = command.execute(this.component, callbacks); + return await executor(args); + } catch (e: unknown) { + console.error(`Error executing command "${command.name}":`, e); + return GridCommands.failure(); + } + } + + public async executeCommands( + commands: ExecuteGridAssistantAction[], + customizeResponseText?: CustomizeResponseText, + ): Promise { + if (this._executing) { + throw new Error('executeCommands is already in progress'); + } + + this._executing = true; + this._aborted = false; + + const results: CommandResult[] = []; + const callbacks: CommandCallbacks = { + success: GridCommands.success, + failure: GridCommands.failure, + }; + + for (const { name, args } of commands) { + if (this._aborted) { + results.push({ + status: 'aborted', + message: messageLocalization.format(EXECUTION_ABORT_MESSAGE), + }); + break; + } + + const command = this.commands.get(name); + + if (!command) { + this._executing = false; + throw new Error(`Unknown command: ${name}`); + } + // eslint-disable-next-line no-await-in-loop + const result = await this.executeCommand(command, args, callbacks); + + GridCommands.applyCustomizedResponseText(result, name, args, customizeResponseText); + results.push(result); + } + + this._executing = false; + + return results; } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts index 2428ddeed0aa..2f0a7ddad218 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts @@ -1,13 +1,21 @@ import type { InternalGrid } from '@ts/grids/grid_core/m_types'; import type { ZodObject, ZodRawShape } from 'zod'; +import type { JsonSchema7Type } from 'zod-to-json-schema'; -type CommandStatus = 'success' | 'failure' | 'aborted'; +/** JSON Schema draft-07 object sent to the LLM. */ +export type JsonSchema = JsonSchema7Type & { + $schema?: string; +}; + +export type CommandStatus = 'success' | 'failure' | 'aborted'; export interface CommandResult { status: CommandStatus; message: string; } +export type CommandResults = CommandResult[]; + export interface CommandCallbacks { success: (message?: string) => CommandResult; failure: (message?: string) => CommandResult; @@ -28,13 +36,12 @@ export interface GridCommand { execute: (component: InternalGrid, callbacks: CommandCallbacks) => CommandExecutor; } -export interface Command { - command: string; - args: Record; -} -export interface CommandResponse { - commands: Command[]; - explanation: string; +export interface CommandMessages { + success: string; + failure: string; } -export type CommandResults = CommandResult[]; +export type CustomizeResponseText = ( + commandName: string, + commandArgs: Record, +) => Partial | undefined; diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index 3240385f5eab..95a4548d92b4 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/bg.json b/packages/devextreme/js/localization/messages/bg.json index 7e5597e098fd..344c8580a5fe 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/ca.json b/packages/devextreme/js/localization/messages/ca.json index 08a57f52b394..d820ab2e7218 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/cs.json b/packages/devextreme/js/localization/messages/cs.json index eb6be4704c96..e5e551e3db55 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/da.json b/packages/devextreme/js/localization/messages/da.json index c90e8546079a..12adf88a77e7 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/de.json b/packages/devextreme/js/localization/messages/de.json index 03e48fb2f97f..d57ab3762741 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/el.json b/packages/devextreme/js/localization/messages/el.json index 61ed1feaa5ec..8a08c122170d 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json index b6b383a02d2f..276cb8dfeddf 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/es.json b/packages/devextreme/js/localization/messages/es.json index 74a04007ac6c..a6c2b6601400 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/fa.json b/packages/devextreme/js/localization/messages/fa.json index b9477c8a7480..6d8dbeeaea96 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/fi.json b/packages/devextreme/js/localization/messages/fi.json index 79f9662a525a..79bca31153e1 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/fr.json b/packages/devextreme/js/localization/messages/fr.json index c2e0ba65e8f5..211de17db919 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/hu.json b/packages/devextreme/js/localization/messages/hu.json index c19ab61015f1..96c8b4987d38 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/it.json b/packages/devextreme/js/localization/messages/it.json index 833f187198c4..209acb848652 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/ja.json b/packages/devextreme/js/localization/messages/ja.json index 9a3236c16f1b..a943d2a2a681 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/ko.json b/packages/devextreme/js/localization/messages/ko.json index bef4d83e95e5..05a6dec18786 100644 --- a/packages/devextreme/js/localization/messages/ko.json +++ b/packages/devextreme/js/localization/messages/ko.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/lt.json b/packages/devextreme/js/localization/messages/lt.json index 4ba5e5e5f9a8..c0cd590e862c 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/lv.json b/packages/devextreme/js/localization/messages/lv.json index 2f55b92bab0e..3f9d49141d6e 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/nb.json b/packages/devextreme/js/localization/messages/nb.json index 7adda3e3760b..2b1379c1461f 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/nl.json b/packages/devextreme/js/localization/messages/nl.json index fed6f5f6ce61..1e3137f4662e 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/pl.json b/packages/devextreme/js/localization/messages/pl.json index 0a02e8fcd5fe..55459f1e00fc 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/pt.json b/packages/devextreme/js/localization/messages/pt.json index d56a1088d875..f3a342511b3d 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/ro.json b/packages/devextreme/js/localization/messages/ro.json index cfd91f738fba..4d7e6835420b 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/ru.json b/packages/devextreme/js/localization/messages/ru.json index 58b41749ee3c..a2679472af63 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/sl.json b/packages/devextreme/js/localization/messages/sl.json index 7829e68d5656..2c5a502f5845 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/sv.json b/packages/devextreme/js/localization/messages/sv.json index e5e129c681b5..c68cec2e3b59 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/tr.json b/packages/devextreme/js/localization/messages/tr.json index 0575de043e38..461e455463d0 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/uk.json b/packages/devextreme/js/localization/messages/uk.json index 59400ae3a626..310b10fb8acc 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/vi.json b/packages/devextreme/js/localization/messages/vi.json index faa69101d991..778713b2007b 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/zh-tw.json b/packages/devextreme/js/localization/messages/zh-tw.json index de9b789430ea..7a25712ce126 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/js/localization/messages/zh.json b/packages/devextreme/js/localization/messages/zh.json index 4721df8bfafb..bbad56686352 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -112,6 +112,8 @@ "dxDataGrid-aiAssistantProcessingMessage": "Processing...", "dxDataGrid-aiAssistantErrorMessageHeader": "Failed to process request", "dxDataGrid-aiAssistantSuccessMessage": "Success", + "dxDataGrid-aiAssistantErrorMessage": "Error", + "dxDataGrid-aiAssistantExecutionAbortMessage": "Execution Interrupted", "dxDataGrid-aiAssistantClearButtonText": "Clear", "dxDataGrid-aiAssistantRegenerateButtonText": "Regenerate", "dxDataGrid-aiAChatEmptyViewMessage": "Chat is Empty", diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 71d919974a68..18f14cc2489b 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -65,7 +65,8 @@ "jszip": "^3.10.1", "rrule": "^2.7.1", "unplugin": "^3.0.0", - "zod": "~3.24.4" + "zod": "3.24.4", + "zod-to-json-schema": "3.24.6" }, "devDependencies": { "@babel/core": "7.29.0", @@ -73,8 +74,8 @@ "@babel/parser": "7.29.2", "@babel/plugin-proposal-decorators": "7.29.0", "@babel/plugin-transform-modules-commonjs": "7.28.6", - "@babel/plugin-transform-typescript": "7.28.6", "@babel/plugin-transform-runtime": "7.29.0", + "@babel/plugin-transform-typescript": "7.28.6", "@babel/preset-env": "7.29.2", "@devextreme-generator/angular": "3.0.12", "@devextreme-generator/build-helpers": "3.0.12", @@ -214,8 +215,8 @@ "typescript-min": "npm:typescript@4.9.5", "uuid": "14.0.0", "vinyl": "2.2.1", - "vite": "8.0.8", "vinyl-named": "1.1.0", + "vite": "8.0.8", "webpack": "5.105.4", "webpack-stream": "7.0.0", "yaml": "2.8.3", diff --git a/packages/devextreme/testing/runner/lib/pages.ts b/packages/devextreme/testing/runner/lib/pages.ts index 7abc68ac899a..487577c4e3f4 100644 --- a/packages/devextreme/testing/runner/lib/pages.ts +++ b/packages/devextreme/testing/runner/lib/pages.ts @@ -178,10 +178,17 @@ export function createPagesRenderer({ json: '/packages/devextreme/node_modules/systemjs-plugin-json/json.js', 'plugin-babel': '/packages/devextreme/node_modules/systemjs-plugin-babel/plugin-babel.js', 'systemjs-babel-build': '/packages/devextreme/node_modules/systemjs-plugin-babel/systemjs-babel-browser.js', + // eslint-disable-next-line spellcheck/spell-checker + zod: '/packages/devextreme/node_modules/zod/lib', + 'zod-to-json-schema': '/packages/devextreme/node_modules/zod-to-json-schema/dist/cjs', ...cspMap, }; - const systemPackages: Record = { + const systemPackages: Record = { '': { defaultExtension: 'js', }, @@ -202,6 +209,17 @@ export function createPagesRenderer({ events: { main: 'index', }, + // eslint-disable-next-line spellcheck/spell-checker + zod: { + main: 'index.js', + defaultExtension: 'js', + format: 'cjs', + }, + 'zod-to-json-schema': { + main: 'index.js', + defaultExtension: 'js', + format: 'cjs', + }, }; const knockoutPath = '/packages/devextreme/node_modules/knockout/build/output/knockout-latest.debug.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 345327887735..123508311aaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1385,8 +1385,11 @@ importers: specifier: ^3.0.0 version: 3.0.0 zod: - specifier: ~3.24.4 + specifier: 3.24.4 version: 3.24.4 + zod-to-json-schema: + specifier: 3.24.6 + version: 3.24.6(zod@3.24.4) devDependencies: '@babel/core': specifier: 7.29.0 @@ -18092,6 +18095,11 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -41656,6 +41664,10 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 + zod-to-json-schema@3.24.6(zod@3.24.4): + dependencies: + zod: 3.24.4 + zod-to-json-schema@3.25.2(zod@4.1.13): dependencies: zod: 4.1.13