first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import { getConfigFromMetaTag, getAndParseConfigFromMetaTag } from '../metaTagConfig';
|
||||
|
||||
describe('metaTagConfig', () => {
|
||||
beforeEach(() => {
|
||||
document.head.innerHTML = '';
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create and insert a meta tag into the document head
|
||||
*/
|
||||
function createMetaTag(configName: string, content?: string): void {
|
||||
const metaTag = document.createElement('meta');
|
||||
metaTag.setAttribute('name', `n8n:config:${configName}`);
|
||||
|
||||
if (content !== undefined) {
|
||||
metaTag.setAttribute('content', content);
|
||||
}
|
||||
|
||||
document.head.appendChild(metaTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a meta tag with base64-encoded content
|
||||
*/
|
||||
function createMetaTagWithBase64Content(configName: string, value: string): void {
|
||||
const base64Value = btoa(value);
|
||||
createMetaTag(configName, base64Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a meta tag with JSON content (base64-encoded)
|
||||
*/
|
||||
function createMetaTagWithJsonContent(configName: string, value: unknown): void {
|
||||
const jsonString = JSON.stringify(value);
|
||||
createMetaTagWithBase64Content(configName, jsonString);
|
||||
}
|
||||
|
||||
describe('getConfigFromMetaTag', () => {
|
||||
it('should return null when meta tag does not exist', () => {
|
||||
const result = getConfigFromMetaTag('testConfig');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when meta tag exists but has no content attribute', () => {
|
||||
createMetaTag('testConfig');
|
||||
|
||||
const result = getConfigFromMetaTag('testConfig');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when meta tag has empty content attribute', () => {
|
||||
createMetaTag('testConfig', '');
|
||||
|
||||
const result = getConfigFromMetaTag('testConfig');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should decode and return base64 content successfully', () => {
|
||||
const originalValue = 'Hello World';
|
||||
createMetaTagWithBase64Content('testConfig', originalValue);
|
||||
|
||||
const result = getConfigFromMetaTag('testConfig');
|
||||
expect(result).toBe(originalValue);
|
||||
});
|
||||
|
||||
it('should handle complex string content correctly', () => {
|
||||
const originalValue = 'This is a test with special chars: !@#$%^&*()';
|
||||
createMetaTagWithBase64Content('complexConfig', originalValue);
|
||||
|
||||
const result = getConfigFromMetaTag('complexConfig');
|
||||
expect(result).toBe(originalValue);
|
||||
});
|
||||
|
||||
it('should return null and log warning when base64 decoding fails', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
createMetaTag('invalidConfig', 'invalid-base64!!!');
|
||||
|
||||
const result = getConfigFromMetaTag('invalidConfig');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to read n8n config for "n8n:config:invalidConfig":',
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: 'config1', value: 'value1' },
|
||||
{ name: 'config2', value: 'value2' },
|
||||
{ name: 'special-config', value: 'special-value' },
|
||||
])('should handle different config names correctly: $name', ({ name, value }) => {
|
||||
createMetaTagWithBase64Content(name, value);
|
||||
|
||||
const result = getConfigFromMetaTag(name);
|
||||
expect(result).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAndParseConfigFromMetaTag', () => {
|
||||
it('should return null when meta tag does not exist', () => {
|
||||
const result = getAndParseConfigFromMetaTag('nonExistentConfig');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when getConfigFromMetaTag returns null', () => {
|
||||
createMetaTag('emptyConfig');
|
||||
|
||||
const result = getAndParseConfigFromMetaTag('emptyConfig');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse and return valid JSON object', () => {
|
||||
const originalObject = {
|
||||
key1: 'value1',
|
||||
key2: 42,
|
||||
key3: true,
|
||||
nested: { prop: 'nestedValue' },
|
||||
};
|
||||
createMetaTagWithJsonContent('jsonConfig', originalObject);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag<typeof originalObject>('jsonConfig');
|
||||
expect(result).toEqual(originalObject);
|
||||
});
|
||||
|
||||
it('should parse and return valid JSON array', () => {
|
||||
const originalArray = [1, 2, 3, { name: 'test' }];
|
||||
createMetaTagWithJsonContent('arrayConfig', originalArray);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag<typeof originalArray>('arrayConfig');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
|
||||
it('should parse and return primitive JSON values', () => {
|
||||
const testCases = [
|
||||
{ value: 'simple string', name: 'stringConfig' },
|
||||
{ value: 42, name: 'numberConfig' },
|
||||
{ value: true, name: 'booleanConfig' },
|
||||
{ value: null, name: 'nullConfig' },
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
createMetaTagWithJsonContent(testCase.name, testCase.value);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag(testCase.name);
|
||||
expect(result).toEqual(testCase.value);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null and log warning when JSON parsing fails', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const invalidJson = 'this is not json';
|
||||
createMetaTagWithBase64Content('invalidJsonConfig', invalidJson);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag('invalidJsonConfig');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to parse n8n config for "n8n:config:invalidJsonConfig":',
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle complex nested objects with type safety', () => {
|
||||
interface TestConfig {
|
||||
api: {
|
||||
url: string;
|
||||
version: number;
|
||||
features: string[];
|
||||
};
|
||||
user: {
|
||||
id: number;
|
||||
permissions: Record<string, boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
const originalConfig: TestConfig = {
|
||||
api: {
|
||||
url: 'https://api.example.com',
|
||||
version: 2,
|
||||
features: ['feature1', 'feature2'],
|
||||
},
|
||||
user: {
|
||||
id: 123,
|
||||
permissions: {
|
||||
read: true,
|
||||
write: false,
|
||||
admin: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createMetaTagWithJsonContent('complexConfig', originalConfig);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag<TestConfig>('complexConfig');
|
||||
expect(result).toEqual(originalConfig);
|
||||
|
||||
assert(result);
|
||||
// Type assertions to ensure type safety works
|
||||
expect(result.api.url).toBe('https://api.example.com');
|
||||
expect(result.user.permissions.read).toBe(true);
|
||||
expect(result.api.features).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle empty JSON objects and arrays', () => {
|
||||
const testCases = [
|
||||
{ value: {}, name: 'emptyObject' },
|
||||
{ value: [], name: 'emptyArray' },
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
createMetaTagWithJsonContent(testCase.name, testCase.value);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag(testCase.name);
|
||||
expect(result).toEqual(testCase.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should handle multiple config retrieval operations', () => {
|
||||
const configs = {
|
||||
stringConfig: 'test string',
|
||||
objectConfig: { key: 'value', num: 42 },
|
||||
arrayConfig: [1, 2, 3],
|
||||
};
|
||||
|
||||
Object.entries(configs).forEach(([name, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
createMetaTagWithBase64Content(name, value);
|
||||
} else {
|
||||
createMetaTagWithJsonContent(name, value);
|
||||
}
|
||||
});
|
||||
|
||||
const stringResult = getConfigFromMetaTag('stringConfig');
|
||||
expect(stringResult).toBe('test string');
|
||||
|
||||
const objectResult = getAndParseConfigFromMetaTag('objectConfig');
|
||||
expect(objectResult).toEqual({ key: 'value', num: 42 });
|
||||
|
||||
const arrayResult = getAndParseConfigFromMetaTag('arrayConfig');
|
||||
expect(arrayResult).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should handle edge case of config name with special characters', () => {
|
||||
const configName = 'config-with-dashes_and_underscores.123';
|
||||
const configValue = { test: true };
|
||||
createMetaTagWithJsonContent(configName, configValue);
|
||||
|
||||
const result = getAndParseConfigFromMetaTag(configName);
|
||||
expect(result).toEqual(configValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' });
|
||||
@@ -0,0 +1,54 @@
|
||||
export const STORES = {
|
||||
COMMUNITY_NODES: 'communityNodes',
|
||||
ROOT: 'root',
|
||||
SETTINGS: 'settings',
|
||||
UI: 'ui',
|
||||
USERS: 'users',
|
||||
WORKFLOWS: 'workflows',
|
||||
WORKFLOWS_LIST: 'workflowsList',
|
||||
WORKFLOWS_V2: 'workflowsV2',
|
||||
WORKFLOWS_EE: 'workflowsEE',
|
||||
WORKFLOW_DOCUMENTS: 'workflowDocuments',
|
||||
EXECUTIONS: 'executions',
|
||||
NDV: 'ndv',
|
||||
TEMPLATES: 'templates',
|
||||
NODE_TYPES: 'nodeTypes',
|
||||
CREDENTIALS: 'credentials',
|
||||
TAGS: 'tags',
|
||||
ANNOTATION_TAGS: 'annotationTags',
|
||||
VERSIONS: 'versions',
|
||||
NODE_CREATOR: 'nodeCreator',
|
||||
WEBHOOKS: 'webhooks',
|
||||
HISTORY: 'history',
|
||||
CLOUD_PLAN: 'cloudPlan',
|
||||
RBAC: 'rbac',
|
||||
PUSH: 'push',
|
||||
COLLABORATION: 'collaboration',
|
||||
ASSISTANT: 'assistant',
|
||||
BUILDER: 'builder',
|
||||
CHAT_PANEL: 'chatPanel',
|
||||
CHAT_PANEL_STATE: 'chatPanelState',
|
||||
BECOME_TEMPLATE_CREATOR: 'becomeTemplateCreator',
|
||||
PROJECTS: 'projects',
|
||||
API_KEYS: 'apiKeys',
|
||||
EVALUATION: 'evaluation',
|
||||
FOLDERS: 'folders',
|
||||
MODULES: 'modules',
|
||||
FOCUS_PANEL: 'focusPanel',
|
||||
WORKFLOW_STATE: 'workflowState',
|
||||
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
|
||||
PERSONALIZED_TEMPLATES: 'personalizedTemplates',
|
||||
EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows',
|
||||
EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2: 'readyToRunWorkflowsV2',
|
||||
EXPERIMENT_TEMPLATE_RECO_V2: 'templateRecoV2',
|
||||
PERSONALIZED_TEMPLATES_V3: 'personalizedTemplatesV3',
|
||||
READY_TO_RUN: 'readyToRun',
|
||||
TEMPLATES_DATA_QUALITY: 'templatesDataQuality',
|
||||
BANNERS: 'banners',
|
||||
CONSENT: 'consent',
|
||||
CHAT_HUB: 'chatHub',
|
||||
EXPERIMENT_EMPTY_STATE_BUILDER_PROMPT: 'emptyStateBuilderPrompt',
|
||||
EXPERIMENT_CREDENTIALS_APP_SELECTION: 'credentialsAppSelection',
|
||||
SETUP_PANEL: 'setupPanel',
|
||||
FOCUSED_NODES: 'focusedNodes',
|
||||
} as const;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,46 @@
|
||||
function getTagName(configName: string): string {
|
||||
return `n8n:config:${configName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to read and decode base64-encoded configuration values from meta tags
|
||||
*/
|
||||
export function getConfigFromMetaTag(configName: string): string | null {
|
||||
const tagName = getTagName(configName);
|
||||
|
||||
try {
|
||||
const metaTag = document.querySelector(`meta[name="${tagName}"]`);
|
||||
if (!metaTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedContent = metaTag.getAttribute('content');
|
||||
if (!encodedContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decode base64 content
|
||||
const content = atob(encodedContent);
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read n8n config for "${tagName}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to read and parse configuration values from meta tags
|
||||
*/
|
||||
export function getAndParseConfigFromMetaTag<T>(configName: string): T | null {
|
||||
const config = getConfigFromMetaTag(configName);
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(config) as T;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse n8n config for "${getTagName(configName)}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
env: {
|
||||
DEV: boolean;
|
||||
PROD: boolean;
|
||||
NODE_ENV: 'development' | 'production';
|
||||
VUE_APP_URL_BASE_API: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Window {
|
||||
BASE_PATH: string;
|
||||
REST_ENDPOINT: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
import {
|
||||
type IAgentRequestStoreState,
|
||||
type IAgentRequest,
|
||||
useAgentRequestStore,
|
||||
} from './useAgentRequestStore';
|
||||
|
||||
// Mock localStorage
|
||||
let mockLocalStorageValue: IAgentRequestStoreState = {};
|
||||
|
||||
const NODE_NAME = 'Test Node';
|
||||
const NODE_ID_1 = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const NODE_ID_2 = '987fcdeb-51a2-43d7-b654-987654321000';
|
||||
const NODE_ID_3 = '456abcde-f789-12d3-a456-426614174000';
|
||||
|
||||
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(() => {
|
||||
mockLocalStorageValue = {};
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('initializes with empty state when localStorage is empty', () => {
|
||||
const store = useAgentRequestStore();
|
||||
expect(store.agentRequests.value).toEqual({});
|
||||
});
|
||||
|
||||
it('initializes with data from localStorage', () => {
|
||||
const mockData: IAgentRequestStoreState = {
|
||||
'workflow-1': {
|
||||
[NODE_ID_1]: { query: { [NODE_NAME]: { param1: 'value1' } } },
|
||||
},
|
||||
};
|
||||
mockLocalStorageValue = mockData;
|
||||
|
||||
const store = useAgentRequestStore();
|
||||
expect(store.agentRequests.value).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Getters', () => {
|
||||
it('gets parameter overrides for a node', () => {
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1', param2: 'value2' } },
|
||||
});
|
||||
|
||||
const overrides = store.getAgentRequests('workflow-1', NODE_ID_1);
|
||||
expect(overrides).toEqual({ [NODE_NAME]: { param1: 'value1', param2: 'value2' } });
|
||||
});
|
||||
|
||||
it('returns empty object for non-existent workflow/node', () => {
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
const overrides = store.getAgentRequests('non-existent', NODE_ID_1);
|
||||
expect(overrides).toEqual({});
|
||||
});
|
||||
|
||||
it('gets a specific parameter override', () => {
|
||||
const store = useAgentRequestStore();
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1', param2: 'value2' } },
|
||||
});
|
||||
|
||||
const override = store.getQueryValue('workflow-1', NODE_ID_1, NODE_NAME, 'param1');
|
||||
expect(override).toBe('value1');
|
||||
});
|
||||
|
||||
it('returns undefined for non-existent parameter', () => {
|
||||
const store = useAgentRequestStore();
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1' } },
|
||||
});
|
||||
|
||||
const override = store.getQueryValue('workflow-1', NODE_ID_1, NODE_NAME, 'non-existent');
|
||||
expect(override).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('sets parameter overrides for a node', () => {
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1', param2: 'value2' } },
|
||||
});
|
||||
|
||||
expect(
|
||||
(store.agentRequests.value['workflow-1'] as unknown as { [key: string]: IAgentRequest })[
|
||||
NODE_ID_1
|
||||
].query,
|
||||
).toEqual({
|
||||
[NODE_NAME]: { param1: 'value1', param2: 'value2' },
|
||||
});
|
||||
});
|
||||
|
||||
it('clears parameter overrides for a node', () => {
|
||||
const store = useAgentRequestStore();
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1', param2: 'value2' } },
|
||||
});
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_2, {
|
||||
query: { [NODE_NAME]: { param3: 'value3' } },
|
||||
});
|
||||
|
||||
store.clearAgentRequests('workflow-1', NODE_ID_1);
|
||||
|
||||
expect(
|
||||
(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({
|
||||
[NODE_NAME]: { param3: 'value3' },
|
||||
});
|
||||
});
|
||||
|
||||
it('clears all parameter overrides for a workflow', () => {
|
||||
const store = useAgentRequestStore();
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1' } },
|
||||
});
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_2, {
|
||||
query: { [NODE_NAME]: { param2: 'value2' } },
|
||||
});
|
||||
store.setAgentRequestForNode('workflow-2', NODE_ID_3, {
|
||||
query: { [NODE_NAME]: { param3: 'value3' } },
|
||||
});
|
||||
|
||||
store.clearAllAgentRequests('workflow-1');
|
||||
|
||||
expect(store.agentRequests.value['workflow-1']).toEqual({});
|
||||
expect(store.agentRequests.value['workflow-2']).toEqual({
|
||||
[NODE_ID_3]: { query: { [NODE_NAME]: { param3: 'value3' } } },
|
||||
});
|
||||
});
|
||||
|
||||
it('clears all parameter overrides when no workflowId is provided', () => {
|
||||
const store = useAgentRequestStore();
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1' } },
|
||||
});
|
||||
store.setAgentRequestForNode('workflow-2', NODE_ID_2, {
|
||||
query: { [NODE_NAME]: { param2: 'value2' } },
|
||||
});
|
||||
|
||||
store.clearAllAgentRequests();
|
||||
|
||||
expect(store.agentRequests.value).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Persistence', () => {
|
||||
it('saves to localStorage when state changes', async () => {
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.setAgentRequestForNode('workflow-1', NODE_ID_1, {
|
||||
query: { [NODE_NAME]: { param1: 'value1' } },
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockLocalStorageValue).toEqual({
|
||||
'workflow-1': {
|
||||
[NODE_ID_1]: { query: { [NODE_NAME]: { param1: 'value1' } } },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import type { AgentRequestQuery } from 'n8n-workflow';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
const LOCAL_STORAGE_AGENT_REQUESTS = 'N8N_AGENT_REQUESTS';
|
||||
|
||||
export interface IAgentRequest {
|
||||
query: AgentRequestQuery;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
export interface IAgentRequestStoreState {
|
||||
[workflowId: string]: {
|
||||
[nodeName: string]: IAgentRequest;
|
||||
};
|
||||
}
|
||||
|
||||
export const useAgentRequestStore = defineStore('agentRequest', () => {
|
||||
// State
|
||||
const agentRequests = useLocalStorage<IAgentRequestStoreState>(LOCAL_STORAGE_AGENT_REQUESTS, {});
|
||||
|
||||
// Helper function to ensure workflow and node entries exist
|
||||
const ensureWorkflowAndNodeExist = (workflowId: string, nodeId: string): void => {
|
||||
if (!agentRequests.value[workflowId]) {
|
||||
agentRequests.value[workflowId] = {};
|
||||
}
|
||||
|
||||
if (!agentRequests.value[workflowId][nodeId]) {
|
||||
agentRequests.value[workflowId][nodeId] = { query: {} };
|
||||
}
|
||||
};
|
||||
|
||||
// Getters
|
||||
const getAgentRequests = (workflowId: string, nodeId: string): IAgentRequest['query'] => {
|
||||
return agentRequests.value[workflowId]?.[nodeId]?.query || {};
|
||||
};
|
||||
|
||||
const getQueryValue = (
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
nodeName: string,
|
||||
paramName?: string,
|
||||
): unknown => {
|
||||
const query = agentRequests.value[workflowId]?.[nodeId]?.query?.[nodeName];
|
||||
if (typeof query === 'string' || !paramName) {
|
||||
return query;
|
||||
}
|
||||
return query?.[paramName];
|
||||
};
|
||||
|
||||
const setAgentRequestForNode = (
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
request: IAgentRequest,
|
||||
): void => {
|
||||
ensureWorkflowAndNodeExist(workflowId, nodeId);
|
||||
|
||||
agentRequests.value[workflowId][nodeId] = {
|
||||
...request,
|
||||
query: { ...request.query },
|
||||
};
|
||||
};
|
||||
|
||||
const clearAgentRequests = (workflowId: string, nodeId: string): void => {
|
||||
if (agentRequests.value[workflowId]) {
|
||||
agentRequests.value[workflowId][nodeId] = { query: {} };
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllAgentRequests = (workflowId?: string): void => {
|
||||
if (workflowId) {
|
||||
agentRequests.value[workflowId] = {};
|
||||
} else {
|
||||
agentRequests.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
const getAgentRequest = (workflowId: string, nodeId: string): IAgentRequest | undefined => {
|
||||
if (agentRequests.value[workflowId]) return agentRequests.value[workflowId]?.[nodeId];
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
agentRequests,
|
||||
getAgentRequests,
|
||||
getQueryValue,
|
||||
setAgentRequestForNode,
|
||||
clearAgentRequests,
|
||||
clearAllAgentRequests,
|
||||
getAgentRequest,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import { randomString, setGlobalState } from 'n8n-workflow';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { STORES } from './constants';
|
||||
import { getConfigFromMetaTag } from './metaTagConfig';
|
||||
|
||||
const { VUE_APP_URL_BASE_API } = import.meta.env;
|
||||
|
||||
export type RootStoreState = {
|
||||
baseUrl: string;
|
||||
restEndpoint: string;
|
||||
defaultLocale: string;
|
||||
endpointForm: string;
|
||||
endpointFormTest: string;
|
||||
endpointFormWaiting: string;
|
||||
endpointMcp: string;
|
||||
endpointMcpTest: string;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
endpointWebhookWaiting: string;
|
||||
timezone: string;
|
||||
executionTimeout: number;
|
||||
maxExecutionTimeout: number;
|
||||
versionCli: string;
|
||||
oauthCallbackUrls: object;
|
||||
n8nMetadata: {
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
pushRef: string;
|
||||
urlBaseWebhook: string;
|
||||
urlBaseEditor: string;
|
||||
instanceId: string;
|
||||
binaryDataMode: 'default' | 'filesystem' | 's3' | 'database';
|
||||
};
|
||||
|
||||
export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||
// Generate or retrieve client ID from sessionStorage
|
||||
const getClientId = (): string => {
|
||||
const storageKey = 'n8n-client-id';
|
||||
const existingId = sessionStorage.getItem(storageKey);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
const newId = randomString(10).toLowerCase();
|
||||
sessionStorage.setItem(storageKey, newId);
|
||||
return newId;
|
||||
};
|
||||
|
||||
const state = ref<RootStoreState>({
|
||||
baseUrl: VUE_APP_URL_BASE_API ?? window.BASE_PATH,
|
||||
restEndpoint: getConfigFromMetaTag('rest-endpoint') ?? 'rest',
|
||||
defaultLocale: 'en',
|
||||
endpointForm: 'form',
|
||||
endpointFormTest: 'form-test',
|
||||
endpointFormWaiting: 'form-waiting',
|
||||
endpointMcp: 'mcp',
|
||||
endpointMcpTest: 'mcp-test',
|
||||
endpointWebhook: 'webhook',
|
||||
endpointWebhookTest: 'webhook-test',
|
||||
endpointWebhookWaiting: 'webhook-waiting',
|
||||
timezone: 'America/New_York',
|
||||
executionTimeout: -1,
|
||||
maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
|
||||
versionCli: '0.0.0',
|
||||
oauthCallbackUrls: {},
|
||||
n8nMetadata: {},
|
||||
pushRef: getClientId(),
|
||||
urlBaseWebhook: 'http://localhost:5678/',
|
||||
urlBaseEditor: 'http://localhost:5678',
|
||||
instanceId: '',
|
||||
binaryDataMode: 'default',
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #region Computed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const baseUrl = computed(() => state.value.baseUrl);
|
||||
|
||||
const formUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointForm}`);
|
||||
|
||||
const formTestUrl = computed(() => `${state.value.urlBaseEditor}${state.value.endpointFormTest}`);
|
||||
|
||||
const formWaitingUrl = computed(
|
||||
() => `${state.value.urlBaseEditor}${state.value.endpointFormWaiting}`,
|
||||
);
|
||||
|
||||
const webhookUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointWebhook}`);
|
||||
|
||||
const webhookTestUrl = computed(
|
||||
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookTest}`,
|
||||
);
|
||||
|
||||
const webhookWaitingUrl = computed(
|
||||
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookWaiting}`,
|
||||
);
|
||||
|
||||
const mcpUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointMcp}`);
|
||||
|
||||
const mcpTestUrl = computed(() => `${state.value.urlBaseEditor}${state.value.endpointMcpTest}`);
|
||||
|
||||
const pushRef = computed(() => state.value.pushRef);
|
||||
|
||||
const binaryDataMode = computed(() => state.value.binaryDataMode);
|
||||
|
||||
const defaultLocale = computed(() => state.value.defaultLocale);
|
||||
|
||||
const urlBaseEditor = computed(() => state.value.urlBaseEditor);
|
||||
|
||||
const instanceId = computed(() => state.value.instanceId);
|
||||
|
||||
const versionCli = computed(() => state.value.versionCli);
|
||||
|
||||
const OAuthCallbackUrls = computed(() => state.value.oauthCallbackUrls);
|
||||
|
||||
const restUrl = computed(() => `${state.value.baseUrl}${state.value.restEndpoint}`);
|
||||
|
||||
const executionTimeout = computed(() => state.value.executionTimeout);
|
||||
|
||||
const maxExecutionTimeout = computed(() => state.value.maxExecutionTimeout);
|
||||
|
||||
const timezone = computed(() => state.value.timezone);
|
||||
|
||||
const restApiContext = computed(() => ({
|
||||
baseUrl: restUrl.value,
|
||||
pushRef: state.value.pushRef,
|
||||
}));
|
||||
|
||||
// #endregion
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #region Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const setUrlBaseWebhook = (value: string) => {
|
||||
const url = value.endsWith('/') ? value : `${value}/`;
|
||||
state.value.urlBaseWebhook = url;
|
||||
};
|
||||
|
||||
const setUrlBaseEditor = (value: string) => {
|
||||
const url = value.endsWith('/') ? value : `${value}/`;
|
||||
state.value.urlBaseEditor = url;
|
||||
};
|
||||
|
||||
const setEndpointForm = (value: string) => {
|
||||
state.value.endpointForm = value;
|
||||
};
|
||||
|
||||
const setEndpointFormTest = (value: string) => {
|
||||
state.value.endpointFormTest = value;
|
||||
};
|
||||
|
||||
const setEndpointFormWaiting = (value: string) => {
|
||||
state.value.endpointFormWaiting = value;
|
||||
};
|
||||
|
||||
const setEndpointWebhook = (value: string) => {
|
||||
state.value.endpointWebhook = value;
|
||||
};
|
||||
|
||||
const setEndpointWebhookTest = (value: string) => {
|
||||
state.value.endpointWebhookTest = value;
|
||||
};
|
||||
|
||||
const setEndpointWebhookWaiting = (value: string) => {
|
||||
state.value.endpointWebhookWaiting = value;
|
||||
};
|
||||
|
||||
const setEndpointMcp = (value: string) => {
|
||||
state.value.endpointMcp = value;
|
||||
};
|
||||
|
||||
const setEndpointMcpTest = (value: string) => {
|
||||
state.value.endpointMcpTest = value;
|
||||
};
|
||||
|
||||
const setTimezone = (value: string) => {
|
||||
state.value.timezone = value;
|
||||
setGlobalState({ defaultTimezone: value });
|
||||
};
|
||||
|
||||
const setExecutionTimeout = (value: number) => {
|
||||
state.value.executionTimeout = value;
|
||||
};
|
||||
|
||||
const setMaxExecutionTimeout = (value: number) => {
|
||||
state.value.maxExecutionTimeout = value;
|
||||
};
|
||||
|
||||
const setVersionCli = (value: string) => {
|
||||
state.value.versionCli = value;
|
||||
};
|
||||
|
||||
const setInstanceId = (value: string) => {
|
||||
state.value.instanceId = value;
|
||||
};
|
||||
|
||||
const setOauthCallbackUrls = (value: RootStoreState['oauthCallbackUrls']) => {
|
||||
state.value.oauthCallbackUrls = value;
|
||||
};
|
||||
|
||||
const setN8nMetadata = (value: RootStoreState['n8nMetadata']) => {
|
||||
state.value.n8nMetadata = value;
|
||||
};
|
||||
|
||||
const setDefaultLocale = (value: string) => {
|
||||
state.value.defaultLocale = value;
|
||||
};
|
||||
|
||||
const setBinaryDataMode = (value: RootStoreState['binaryDataMode']) => {
|
||||
state.value.binaryDataMode = value;
|
||||
};
|
||||
|
||||
// #endregion
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
formUrl,
|
||||
formTestUrl,
|
||||
formWaitingUrl,
|
||||
mcpUrl,
|
||||
mcpTestUrl,
|
||||
webhookUrl,
|
||||
webhookTestUrl,
|
||||
webhookWaitingUrl,
|
||||
restUrl,
|
||||
restApiContext,
|
||||
urlBaseEditor,
|
||||
versionCli,
|
||||
instanceId,
|
||||
pushRef,
|
||||
defaultLocale,
|
||||
binaryDataMode,
|
||||
OAuthCallbackUrls,
|
||||
executionTimeout,
|
||||
maxExecutionTimeout,
|
||||
timezone,
|
||||
setUrlBaseWebhook,
|
||||
setUrlBaseEditor,
|
||||
setEndpointForm,
|
||||
setEndpointFormTest,
|
||||
setEndpointFormWaiting,
|
||||
setEndpointWebhook,
|
||||
setEndpointWebhookTest,
|
||||
setEndpointWebhookWaiting,
|
||||
setEndpointMcp,
|
||||
setEndpointMcpTest,
|
||||
setTimezone,
|
||||
setExecutionTimeout,
|
||||
setMaxExecutionTimeout,
|
||||
setVersionCli,
|
||||
setInstanceId,
|
||||
setOauthCallbackUrls,
|
||||
setN8nMetadata,
|
||||
setDefaultLocale,
|
||||
setBinaryDataMode,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user