mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
refactor: Simplify agent request store (#15743)
This commit is contained in:
7
packages/frontend/@n8n/stores/src/__tests__/setup.ts
Normal file
7
packages/frontend/@n8n/stores/src/__tests__/setup.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { configure } from '@testing-library/vue';
|
||||||
|
|
||||||
|
// Avoid tests failing because of difference between local and GitHub actions timezone
|
||||||
|
process.env.TZ = 'UTC';
|
||||||
|
|
||||||
|
configure({ testIdAttribute: 'data-test-id' });
|
||||||
@@ -2,208 +2,180 @@ import { setActivePinia, createPinia } from 'pinia';
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
import { useAgentRequestStore } from './useAgentRequestStore';
|
import {
|
||||||
|
type IAgentRequestStoreState,
|
||||||
|
type IAgentRequest,
|
||||||
|
useAgentRequestStore,
|
||||||
|
} from './useAgentRequestStore';
|
||||||
|
|
||||||
// Mock localStorage
|
// Mock localStorage
|
||||||
const localStorageMock = {
|
let mockLocalStorageValue: IAgentRequestStoreState = {};
|
||||||
getItem: vi.fn(),
|
|
||||||
setItem: vi.fn(),
|
|
||||||
clear: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'localStorage', {
|
const NODE_ID_1 = '123e4567-e89b-12d3-a456-426614174000';
|
||||||
value: localStorageMock,
|
const NODE_ID_2 = '987fcdeb-51a2-43d7-b654-987654321000';
|
||||||
writable: true,
|
const NODE_ID_3 = '456abcde-f789-12d3-a456-426614174000';
|
||||||
});
|
|
||||||
|
|
||||||
describe('parameterOverrides.store', () => {
|
vi.mock('@vueuse/core', () => ({
|
||||||
|
useLocalStorage: vi.fn((_key, defaultValue) => {
|
||||||
|
if (Object.keys(mockLocalStorageValue).length === 0) {
|
||||||
|
Object.assign(mockLocalStorageValue, structuredClone(defaultValue));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: mockLocalStorageValue,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('agentRequest.store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockLocalStorageValue = {};
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
localStorageMock.getItem.mockReset();
|
|
||||||
localStorageMock.setItem.mockReset();
|
|
||||||
localStorageMock.clear.mockReset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Initialization', () => {
|
describe('Initialization', () => {
|
||||||
it('initializes with empty state when localStorage is empty', () => {
|
it('initializes with empty state when localStorage is empty', () => {
|
||||||
localStorageMock.getItem.mockReturnValue(null);
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
expect(store.agentRequests).toEqual({});
|
expect(store.agentRequests.value).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('initializes with data from localStorage', () => {
|
it('initializes with data from localStorage', () => {
|
||||||
const mockData = {
|
const mockData: IAgentRequestStoreState = {
|
||||||
'workflow-1': {
|
'workflow-1': {
|
||||||
'node-1': { param1: 'value1' },
|
[NODE_ID_1]: { query: { param1: 'value1' } },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
mockLocalStorageValue = mockData;
|
||||||
const store = useAgentRequestStore();
|
|
||||||
expect(store.agentRequests).toEqual(mockData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles localStorage errors gracefully', () => {
|
|
||||||
localStorageMock.getItem.mockImplementation(() => {
|
|
||||||
throw new Error('Storage error');
|
|
||||||
});
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
expect(store.agentRequests).toEqual({});
|
expect(store.agentRequests.value).toEqual(mockData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Getters', () => {
|
describe('Getters', () => {
|
||||||
it('gets parameter overrides for a node', () => {
|
it('gets parameter overrides for a node', () => {
|
||||||
const mockData = {
|
|
||||||
'workflow-1': {
|
|
||||||
'node-1': { param1: 'value1', param2: 'value2' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
|
||||||
const overrides = store.getAgentRequests('workflow-1', 'node-1');
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1', param2: 'value2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const overrides = store.getAgentRequests('workflow-1', NODE_ID_1);
|
||||||
expect(overrides).toEqual({ param1: 'value1', param2: 'value2' });
|
expect(overrides).toEqual({ param1: 'value1', param2: 'value2' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty object for non-existent workflow/node', () => {
|
it('returns empty object for non-existent workflow/node', () => {
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
|
||||||
const overrides = store.getAgentRequests('non-existent', 'node-1');
|
const overrides = store.getAgentRequests('non-existent', NODE_ID_1);
|
||||||
expect(overrides).toEqual({});
|
expect(overrides).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gets a specific parameter override', () => {
|
it('gets a specific parameter override', () => {
|
||||||
const mockData = {
|
|
||||||
'workflow-1': {
|
|
||||||
'node-1': { param1: 'value1', param2: 'value2' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1', param2: 'value2' },
|
||||||
|
});
|
||||||
|
|
||||||
const override = store.getAgentRequest('workflow-1', 'node-1', 'param1');
|
const override = store.getQueryValue('workflow-1', NODE_ID_1, 'param1');
|
||||||
expect(override).toBe('value1');
|
expect(override).toBe('value1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined for non-existent parameter', () => {
|
it('returns undefined for non-existent parameter', () => {
|
||||||
const mockData = {
|
|
||||||
'workflow-1': {
|
|
||||||
'node-1': { param1: 'value1' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1' },
|
||||||
|
});
|
||||||
|
|
||||||
const override = store.getAgentRequest('workflow-1', 'node-1', 'non-existent');
|
const override = store.getQueryValue('workflow-1', NODE_ID_1, 'non-existent');
|
||||||
expect(override).toBeUndefined();
|
expect(override).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles string query type', () => {
|
||||||
|
const store = useAgentRequestStore();
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: 'string-query',
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = store.getAgentRequests('workflow-1', NODE_ID_1);
|
||||||
|
expect(query).toBe('string-query');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Actions', () => {
|
describe('Actions', () => {
|
||||||
it('adds a parameter override', () => {
|
it('sets parameter overrides for a node', () => {
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
|
||||||
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1', param2: 'value2' },
|
||||||
expect(store.agentRequests['workflow-1']['node-1'].param1).toBe('value1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds multiple parameter overrides', () => {
|
expect(
|
||||||
const store = useAgentRequestStore();
|
(store.agentRequests.value['workflow-1'] as unknown as { [key: string]: IAgentRequest })[
|
||||||
|
NODE_ID_1
|
||||||
store.addAgentRequests('workflow-1', 'node-1', {
|
].query,
|
||||||
param1: 'value1',
|
).toEqual({
|
||||||
param2: 'value2',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(store.agentRequests['workflow-1']['node-1']).toEqual({
|
|
||||||
param1: 'value1',
|
param1: 'value1',
|
||||||
param2: 'value2',
|
param2: 'value2',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears parameter overrides for a node', () => {
|
it('clears parameter overrides for a node', () => {
|
||||||
const mockData = {
|
|
||||||
'workflow-1': {
|
|
||||||
'node-1': { param1: 'value1', param2: 'value2' },
|
|
||||||
'node-2': { param3: 'value3' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1', param2: 'value2' },
|
||||||
|
});
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_2, {
|
||||||
|
query: { param3: 'value3' },
|
||||||
|
});
|
||||||
|
|
||||||
store.clearAgentRequests('workflow-1', 'node-1');
|
store.clearAgentRequests('workflow-1', NODE_ID_1);
|
||||||
|
|
||||||
expect(store.agentRequests['workflow-1']['node-1']).toEqual({});
|
expect(
|
||||||
expect(store.agentRequests['workflow-1']['node-2']).toEqual({ param3: 'value3' });
|
(store.agentRequests.value['workflow-1'] as unknown as { [key: string]: IAgentRequest })[
|
||||||
|
NODE_ID_1
|
||||||
|
].query,
|
||||||
|
).toEqual({});
|
||||||
|
expect(
|
||||||
|
(store.agentRequests.value['workflow-1'] as unknown as { [key: string]: IAgentRequest })[
|
||||||
|
NODE_ID_2
|
||||||
|
].query,
|
||||||
|
).toEqual({
|
||||||
|
param3: 'value3',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears all parameter overrides for a workflow', () => {
|
it('clears all parameter overrides for a workflow', () => {
|
||||||
const mockData = {
|
|
||||||
'workflow-1': {
|
|
||||||
'node-1': { param1: 'value1' },
|
|
||||||
'node-2': { param2: 'value2' },
|
|
||||||
},
|
|
||||||
'workflow-2': {
|
|
||||||
'node-3': { param3: 'value3' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1' },
|
||||||
|
});
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_2, {
|
||||||
|
query: { param2: 'value2' },
|
||||||
|
});
|
||||||
|
store.setAgentRequestForNode('workflow-2', NODE_ID_3, {
|
||||||
|
query: { param3: 'value3' },
|
||||||
|
});
|
||||||
|
|
||||||
store.clearAllAgentRequests('workflow-1');
|
store.clearAllAgentRequests('workflow-1');
|
||||||
|
|
||||||
expect(store.agentRequests['workflow-1']).toEqual({});
|
expect(store.agentRequests.value['workflow-1']).toEqual({});
|
||||||
expect(store.agentRequests['workflow-2']).toEqual({
|
expect(store.agentRequests.value['workflow-2']).toEqual({
|
||||||
'node-3': { param3: 'value3' },
|
[NODE_ID_3]: { query: { param3: 'value3' } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears all parameter overrides when no workflowId is provided', () => {
|
it('clears all parameter overrides when no workflowId is provided', () => {
|
||||||
const mockData = {
|
|
||||||
'workflow-1': {
|
|
||||||
'node-1': { param1: 'value1' },
|
|
||||||
},
|
|
||||||
'workflow-2': {
|
|
||||||
'node-2': { param2: 'value2' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1' },
|
||||||
|
});
|
||||||
|
store.setAgentRequestForNode('workflow-2', NODE_ID_2, {
|
||||||
|
query: { param2: 'value2' },
|
||||||
|
});
|
||||||
|
|
||||||
store.clearAllAgentRequests();
|
store.clearAllAgentRequests();
|
||||||
|
|
||||||
expect(store.agentRequests).toEqual({});
|
expect(store.agentRequests.value).toEqual({});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('generateAgentRequest', () => {
|
|
||||||
it('generateAgentRequest', () => {
|
|
||||||
const store = useAgentRequestStore();
|
|
||||||
|
|
||||||
store.addAgentRequests('workflow-1', 'id1', {
|
|
||||||
param1: 'override1',
|
|
||||||
'parent.child': 'override2',
|
|
||||||
'parent.array[0].value': 'overrideArray1',
|
|
||||||
'parent.array[1].value': 'overrideArray2',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = store.generateAgentRequest('workflow-1', 'id1');
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
param1: 'override1',
|
|
||||||
parent: {
|
|
||||||
child: 'override2',
|
|
||||||
array: [
|
|
||||||
{
|
|
||||||
value: 'overrideArray1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'overrideArray2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,37 +183,17 @@ describe('parameterOverrides.store', () => {
|
|||||||
it('saves to localStorage when state changes', async () => {
|
it('saves to localStorage when state changes', async () => {
|
||||||
const store = useAgentRequestStore();
|
const store = useAgentRequestStore();
|
||||||
|
|
||||||
localStorageMock.setItem.mockReset();
|
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||||
|
query: { param1: 'value1' },
|
||||||
|
});
|
||||||
|
|
||||||
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
|
|
||||||
|
|
||||||
// Wait for the next tick to allow the watch to execute
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
expect(mockLocalStorageValue).toEqual({
|
||||||
'n8n-agent-requests',
|
|
||||||
JSON.stringify({
|
|
||||||
'workflow-1': {
|
'workflow-1': {
|
||||||
'node-1': { param1: 'value1' },
|
[NODE_ID_1]: { query: { param1: 'value1' } },
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle localStorage errors when saving', async () => {
|
|
||||||
const store = useAgentRequestStore();
|
|
||||||
|
|
||||||
localStorageMock.setItem.mockReset();
|
|
||||||
|
|
||||||
localStorageMock.setItem.mockImplementation(() => {
|
|
||||||
throw new Error('Storage error');
|
|
||||||
});
|
|
||||||
|
|
||||||
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(store.agentRequests['workflow-1']['node-1'].param1).toBe('value1');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,41 +1,23 @@
|
|||||||
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
import type { INodeParameters, NodeParameterValueType } from 'n8n-workflow';
|
import type { INodeParameters, NodeParameterValueType } from 'n8n-workflow';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
|
|
||||||
interface IAgentRequestStoreState {
|
const LOCAL_STORAGE_AGENT_REQUESTS = 'N8N_AGENT_REQUESTS';
|
||||||
|
|
||||||
|
export interface IAgentRequest {
|
||||||
|
query: INodeParameters | string;
|
||||||
|
toolName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAgentRequestStoreState {
|
||||||
[workflowId: string]: {
|
[workflowId: string]: {
|
||||||
[nodeName: string]: INodeParameters;
|
[nodeName: string]: IAgentRequest;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'n8n-agent-requests';
|
|
||||||
|
|
||||||
export const useAgentRequestStore = defineStore('agentRequest', () => {
|
export const useAgentRequestStore = defineStore('agentRequest', () => {
|
||||||
// State
|
// State
|
||||||
const agentRequests = ref<IAgentRequestStoreState>(loadFromLocalStorage());
|
const agentRequests = useLocalStorage<IAgentRequestStoreState>(LOCAL_STORAGE_AGENT_REQUESTS, {});
|
||||||
|
|
||||||
// Load initial state from localStorage
|
|
||||||
function loadFromLocalStorage(): IAgentRequestStoreState {
|
|
||||||
try {
|
|
||||||
const storedData = localStorage.getItem(STORAGE_KEY);
|
|
||||||
return storedData ? JSON.parse(storedData) : {};
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save state to localStorage whenever it changes
|
|
||||||
watch(
|
|
||||||
agentRequests,
|
|
||||||
(newValue) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save agent requests to localStorage:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper function to ensure workflow and node entries exist
|
// Helper function to ensure workflow and node entries exist
|
||||||
const ensureWorkflowAndNodeExist = (workflowId: string, nodeId: string): void => {
|
const ensureWorkflowAndNodeExist = (workflowId: string, nodeId: string): void => {
|
||||||
@@ -44,167 +26,66 @@ export const useAgentRequestStore = defineStore('agentRequest', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!agentRequests.value[workflowId][nodeId]) {
|
if (!agentRequests.value[workflowId][nodeId]) {
|
||||||
agentRequests.value[workflowId][nodeId] = {};
|
agentRequests.value[workflowId][nodeId] = { query: {} };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
const getAgentRequests = (workflowId: string, nodeId: string): INodeParameters => {
|
const getAgentRequests = (workflowId: string, nodeId: string): INodeParameters | string => {
|
||||||
return agentRequests.value[workflowId]?.[nodeId] || {};
|
return agentRequests.value[workflowId]?.[nodeId]?.query || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAgentRequest = (
|
const getQueryValue = (
|
||||||
workflowId: string,
|
workflowId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
paramName: string,
|
paramName: string,
|
||||||
): NodeParameterValueType | undefined => {
|
): NodeParameterValueType | undefined => {
|
||||||
return agentRequests.value[workflowId]?.[nodeId]?.[paramName];
|
const query = agentRequests.value[workflowId]?.[nodeId]?.query;
|
||||||
|
if (typeof query === 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return query?.[paramName] as NodeParameterValueType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
const setAgentRequestForNode = (
|
||||||
const addAgentRequest = (
|
|
||||||
workflowId: string,
|
workflowId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
paramName: string,
|
request: IAgentRequest,
|
||||||
paramValues: NodeParameterValueType,
|
): void => {
|
||||||
): INodeParameters => {
|
|
||||||
ensureWorkflowAndNodeExist(workflowId, nodeId);
|
ensureWorkflowAndNodeExist(workflowId, nodeId);
|
||||||
|
|
||||||
agentRequests.value[workflowId][nodeId] = {
|
agentRequests.value[workflowId][nodeId] = {
|
||||||
...agentRequests.value[workflowId][nodeId],
|
...request,
|
||||||
[paramName]: paramValues,
|
query: typeof request.query === 'string' ? request.query : { ...request.query },
|
||||||
};
|
|
||||||
|
|
||||||
return agentRequests.value[workflowId][nodeId];
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAgentRequests = (workflowId: string, nodeId: string, params: INodeParameters): void => {
|
|
||||||
ensureWorkflowAndNodeExist(workflowId, nodeId);
|
|
||||||
|
|
||||||
agentRequests.value[workflowId][nodeId] = {
|
|
||||||
...agentRequests.value[workflowId][nodeId],
|
|
||||||
...params,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearAgentRequests = (workflowId: string, nodeId: string): void => {
|
const clearAgentRequests = (workflowId: string, nodeId: string): void => {
|
||||||
if (agentRequests.value[workflowId]) {
|
if (agentRequests.value[workflowId]) {
|
||||||
agentRequests.value[workflowId][nodeId] = {};
|
agentRequests.value[workflowId][nodeId] = { query: {} };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearAllAgentRequests = (workflowId?: string): void => {
|
const clearAllAgentRequests = (workflowId?: string): void => {
|
||||||
if (workflowId) {
|
if (workflowId) {
|
||||||
// Clear requests for a specific workflow
|
|
||||||
agentRequests.value[workflowId] = {};
|
agentRequests.value[workflowId] = {};
|
||||||
} else {
|
} else {
|
||||||
// Clear all requests
|
|
||||||
agentRequests.value = {};
|
agentRequests.value = {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePath(path: string): string[] {
|
const getAgentRequest = (workflowId: string, nodeId: string): IAgentRequest | undefined => {
|
||||||
return path.split('.').reduce((acc: string[], part) => {
|
if (agentRequests.value[workflowId]) return agentRequests.value[workflowId]?.[nodeId];
|
||||||
if (part.includes('[')) {
|
return undefined;
|
||||||
const [arrayName, index] = part.split('[');
|
|
||||||
if (arrayName) acc.push(arrayName);
|
|
||||||
if (index) acc.push(index.replace(']', ''));
|
|
||||||
} else {
|
|
||||||
acc.push(part);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRequestObject(path: string[], value: NodeParameterValueType): INodeParameters {
|
|
||||||
const result: INodeParameters = {};
|
|
||||||
let current = result;
|
|
||||||
|
|
||||||
for (let i = 0; i < path.length - 1; i++) {
|
|
||||||
const part = path[i];
|
|
||||||
const nextPart = path[i + 1];
|
|
||||||
const isArrayIndex = nextPart && !isNaN(Number(nextPart));
|
|
||||||
|
|
||||||
if (isArrayIndex) {
|
|
||||||
if (!current[part]) {
|
|
||||||
current[part] = [];
|
|
||||||
}
|
|
||||||
while ((current[part] as NodeParameterValueType[]).length <= Number(nextPart)) {
|
|
||||||
(current[part] as NodeParameterValueType[]).push({});
|
|
||||||
}
|
|
||||||
} else if (!current[part]) {
|
|
||||||
current[part] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current[part] as INodeParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
current[path[path.length - 1]] = value;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to deep merge objects
|
|
||||||
function deepMerge(target: INodeParameters, source: INodeParameters): INodeParameters {
|
|
||||||
const result = { ...target };
|
|
||||||
|
|
||||||
for (const key in source) {
|
|
||||||
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
||||||
// Recursively merge nested objects
|
|
||||||
result[key] = deepMerge(
|
|
||||||
(result[key] as INodeParameters) || {},
|
|
||||||
source[key] as INodeParameters,
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(source[key])) {
|
|
||||||
// For arrays, merge by index
|
|
||||||
if (Array.isArray(result[key])) {
|
|
||||||
const targetArray = result[key] as NodeParameterValueType[];
|
|
||||||
const sourceArray = source[key] as NodeParameterValueType[];
|
|
||||||
|
|
||||||
// Ensure target array has enough elements
|
|
||||||
while (targetArray.length < sourceArray.length) {
|
|
||||||
targetArray.push({});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge each array item
|
|
||||||
sourceArray.forEach((item, index) => {
|
|
||||||
if (item && typeof item === 'object') {
|
|
||||||
targetArray[index] = deepMerge(
|
|
||||||
(targetArray[index] as INodeParameters) || {},
|
|
||||||
item as INodeParameters,
|
|
||||||
) as NodeParameterValueType;
|
|
||||||
} else {
|
|
||||||
targetArray[index] = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result[key] = source[key];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For primitive values, use source value
|
|
||||||
result[key] = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateAgentRequest = (workflowId: string, nodeId: string): INodeParameters => {
|
|
||||||
const nodeRequests = agentRequests.value[workflowId]?.[nodeId] || {};
|
|
||||||
|
|
||||||
return Object.entries(nodeRequests).reduce(
|
|
||||||
(acc, [path, value]) => deepMerge(acc, buildRequestObject(parsePath(path), value)),
|
|
||||||
{} as INodeParameters,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
agentRequests,
|
agentRequests,
|
||||||
getAgentRequests,
|
getAgentRequests,
|
||||||
getAgentRequest,
|
getQueryValue,
|
||||||
addAgentRequest,
|
setAgentRequestForNode,
|
||||||
addAgentRequests,
|
|
||||||
clearAgentRequests,
|
clearAgentRequests,
|
||||||
clearAllAgentRequests,
|
clearAllAgentRequests,
|
||||||
generateAgentRequest,
|
getAgentRequest,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ describe('FromAiParametersModal', () => {
|
|||||||
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||||
agentRequestStore = useAgentRequestStore();
|
agentRequestStore = useAgentRequestStore();
|
||||||
agentRequestStore.clearAgentRequests = vi.fn();
|
agentRequestStore.clearAgentRequests = vi.fn();
|
||||||
agentRequestStore.addAgentRequests = vi.fn();
|
agentRequestStore.setAgentRequestForNode = vi.fn();
|
||||||
agentRequestStore.generateAgentRequest = vi.fn();
|
agentRequestStore.getAgentRequest = vi.fn();
|
||||||
nodeTypesStore = useNodeTypesStore();
|
nodeTypesStore = useNodeTypesStore();
|
||||||
nodeTypesStore.getNodeParameterOptions = vi.fn().mockResolvedValue(mockTools);
|
nodeTypesStore.getNodeParameterOptions = vi.fn().mockResolvedValue(mockTools);
|
||||||
});
|
});
|
||||||
@@ -214,9 +214,11 @@ describe('FromAiParametersModal', () => {
|
|||||||
|
|
||||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||||
|
|
||||||
expect(agentRequestStore.addAgentRequests).toHaveBeenCalledWith('test-workflow', 'id1', {
|
expect(agentRequestStore.setAgentRequestForNode).toHaveBeenCalledWith('test-workflow', 'id1', {
|
||||||
'query.testBoolean': true,
|
query: {
|
||||||
'query.testParam': 'override',
|
testBoolean: true,
|
||||||
|
testParam: 'override',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,9 +268,11 @@ describe('FromAiParametersModal', () => {
|
|||||||
);
|
);
|
||||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||||
|
|
||||||
expect(agentRequestStore.addAgentRequests).toHaveBeenCalledWith('test-workflow', 'id1', {
|
expect(agentRequestStore.setAgentRequestForNode).toHaveBeenCalledWith('test-workflow', 'id1', {
|
||||||
'query.testBoolean': false,
|
query: {
|
||||||
'query.testParam': 'given value',
|
testBoolean: false,
|
||||||
|
testParam: 'given value',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
import { FROM_AI_PARAMETERS_MODAL_KEY, AI_MCP_TOOL_NODE_TYPE } from '@/constants';
|
import { FROM_AI_PARAMETERS_MODAL_KEY, AI_MCP_TOOL_NODE_TYPE } from '@/constants';
|
||||||
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
|
import { useAgentRequestStore, type IAgentRequest } from '@n8n/stores/useAgentRequestStore';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import {
|
import {
|
||||||
@@ -166,11 +166,8 @@ watch(
|
|||||||
const inputQuery = inputOverrides?.query as IDataObject;
|
const inputQuery = inputOverrides?.query as IDataObject;
|
||||||
const initialValue = inputQuery?.[value.key]
|
const initialValue = inputQuery?.[value.key]
|
||||||
? inputQuery[value.key]
|
? inputQuery[value.key]
|
||||||
: (agentRequestStore.getAgentRequest(
|
: (agentRequestStore.getQueryValue(workflowsStore.workflowId, newNode.id, value.key) ??
|
||||||
workflowsStore.workflowId,
|
mapTypes[type]?.defaultValue);
|
||||||
newNode.id,
|
|
||||||
'query.' + value.key,
|
|
||||||
) ?? mapTypes[type]?.defaultValue);
|
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
name: 'query.' + value.key,
|
name: 'query.' + value.key,
|
||||||
@@ -189,7 +186,7 @@ watch(
|
|||||||
}
|
}
|
||||||
const queryValue =
|
const queryValue =
|
||||||
inputQuery ??
|
inputQuery ??
|
||||||
agentRequestStore.getAgentRequest(workflowsStore.workflowId, newNode.id, 'query') ??
|
agentRequestStore.getQueryValue(workflowsStore.workflowId, newNode.id, 'query') ??
|
||||||
'';
|
'';
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
@@ -216,7 +213,24 @@ const onExecute = async () => {
|
|||||||
const inputValues = inputs.value?.getValues() ?? {};
|
const inputValues = inputs.value?.getValues() ?? {};
|
||||||
|
|
||||||
agentRequestStore.clearAgentRequests(workflowsStore.workflowId, node.value.id);
|
agentRequestStore.clearAgentRequests(workflowsStore.workflowId, node.value.id);
|
||||||
agentRequestStore.addAgentRequests(workflowsStore.workflowId, node.value.id, inputValues);
|
|
||||||
|
// Structure the input values as IAgentRequest
|
||||||
|
const agentRequest: IAgentRequest = {
|
||||||
|
query: {},
|
||||||
|
toolName: inputValues.toolName as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move all query.* fields to query object
|
||||||
|
Object.entries(inputValues).forEach(([key, value]) => {
|
||||||
|
if (key === 'query') {
|
||||||
|
agentRequest.query = value as string;
|
||||||
|
} else if (key.startsWith('query.') && 'string' !== typeof agentRequest.query) {
|
||||||
|
const queryKey = key.replace('query.', '');
|
||||||
|
agentRequest.query[queryKey] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
agentRequestStore.setAgentRequestForNode(workflowsStore.workflowId, node.value.id, agentRequest);
|
||||||
|
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
node_type: node.value.type,
|
node_type: node.value.type,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ vi.mock('@/stores/workflows.store', () => {
|
|||||||
vi.mock('@/stores/parameterOverrides.store', () => {
|
vi.mock('@/stores/parameterOverrides.store', () => {
|
||||||
const storeState: Partial<ReturnType<typeof useAgentRequestStore>> & {} = {
|
const storeState: Partial<ReturnType<typeof useAgentRequestStore>> & {} = {
|
||||||
agentRequests: {},
|
agentRequests: {},
|
||||||
generateAgentRequest: vi.fn(),
|
getAgentRequest: vi.fn(),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
useAgentRequestStore: vi.fn().mockReturnValue(storeState),
|
useAgentRequestStore: vi.fn().mockReturnValue(storeState),
|
||||||
@@ -692,12 +692,12 @@ describe('useRunWorkflow({ router })', () => {
|
|||||||
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
|
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
|
||||||
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(workflowData);
|
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(workflowData);
|
||||||
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
|
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
|
||||||
vi.mocked(agentRequestStore).generateAgentRequest.mockReturnValue(agentRequest);
|
vi.mocked(agentRequestStore).getAgentRequest.mockReturnValue(agentRequest);
|
||||||
// ACT
|
// ACT
|
||||||
const result = await runWorkflow({ destinationNode: 'Test node' });
|
const result = await runWorkflow({ destinationNode: 'Test node' });
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(agentRequestStore.generateAgentRequest).toHaveBeenCalledWith('WorkflowId', 'Test id');
|
expect(agentRequestStore.getAgentRequest).toHaveBeenCalledWith('WorkflowId', 'Test id');
|
||||||
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith({
|
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith({
|
||||||
agentRequest: {
|
agentRequest: {
|
||||||
query: 'query',
|
query: 'query',
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
|||||||
startRunData.destinationNode = options.destinationNode;
|
startRunData.destinationNode = options.destinationNode;
|
||||||
const nodeId = workflowsStore.getNodeByName(options.destinationNode as string)?.id;
|
const nodeId = workflowsStore.getNodeByName(options.destinationNode as string)?.id;
|
||||||
if (workflow.id && nodeId && version === 2) {
|
if (workflow.id && nodeId && version === 2) {
|
||||||
const agentRequest = agentRequestStore.generateAgentRequest(workflow.id, nodeId);
|
const agentRequest = agentRequestStore.getAgentRequest(workflow.id, nodeId);
|
||||||
|
|
||||||
if (agentRequest) {
|
if (agentRequest) {
|
||||||
startRunData.agentRequest = {
|
startRunData.agentRequest = {
|
||||||
|
|||||||
Reference in New Issue
Block a user