mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix(editor): Add missing node parameter values to AI Assistant request (#10788)
This commit is contained in:
parent
a3335e0ecd
commit
d65ade4e92
|
@ -7,6 +7,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
import { createTestWorkflow } from '@/__tests__/mocks';
|
import { createTestWorkflow } from '@/__tests__/mocks';
|
||||||
|
import type { AssignmentCollectionValue } from 'n8n-workflow';
|
||||||
|
|
||||||
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||||
name: 'Duplicate webhook test',
|
name: 'Duplicate webhook test',
|
||||||
|
@ -70,6 +71,163 @@ describe('useWorkflowHelpers', () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getNodeParametersWithResolvedExpressions', () => {
|
||||||
|
it('should correctly detect and resolve expressions in a regular node ', () => {
|
||||||
|
const nodeParameters = {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: '={{ $json.name }}',
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
};
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const resolvedParameters =
|
||||||
|
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
|
||||||
|
expect(resolvedParameters.url).toHaveProperty('resolvedExpressionValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly detect and resolve expressions in a node with assignments (set node) ', () => {
|
||||||
|
const nodeParameters = {
|
||||||
|
mode: 'manual',
|
||||||
|
duplicateItem: false,
|
||||||
|
assignments: {
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: '25d2d012-089b-424d-bfc6-642982a0711f',
|
||||||
|
name: 'date',
|
||||||
|
value:
|
||||||
|
"={{ DateTime.fromFormat('2023-12-12', 'dd/MM/yyyy').toISODate().plus({7, 'days' }) }}",
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
includeOtherFields: false,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const resolvedParameters =
|
||||||
|
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
|
||||||
|
expect(resolvedParameters).toHaveProperty('assignments');
|
||||||
|
const assignments = resolvedParameters.assignments as AssignmentCollectionValue;
|
||||||
|
expect(assignments).toHaveProperty('assignments');
|
||||||
|
expect(assignments.assignments[0].value).toHaveProperty('resolvedExpressionValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly detect and resolve expressions in a node with filter component', () => {
|
||||||
|
const nodeParameters = {
|
||||||
|
mode: 'rules',
|
||||||
|
rules: {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
conditions: {
|
||||||
|
options: {
|
||||||
|
caseSensitive: true,
|
||||||
|
leftValue: '',
|
||||||
|
typeValidation: 'strict',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
leftValue: "={{ $('Edit Fields 1').item.json.name }}",
|
||||||
|
rightValue: 12,
|
||||||
|
operator: {
|
||||||
|
type: 'number',
|
||||||
|
operation: 'equals',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
combinator: 'and',
|
||||||
|
},
|
||||||
|
renameOutput: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
looseTypeValidation: false,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||||
|
nodeParameters,
|
||||||
|
) as typeof nodeParameters;
|
||||||
|
expect(resolvedParameters).toHaveProperty('rules');
|
||||||
|
expect(resolvedParameters.rules).toHaveProperty('values');
|
||||||
|
expect(resolvedParameters.rules.values[0].conditions.conditions[0].leftValue).toHaveProperty(
|
||||||
|
'resolvedExpressionValue',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should correctly detect and resolve expressions in a node with resource locator component', () => {
|
||||||
|
const nodeParameters = {
|
||||||
|
authentication: 'oAuth2',
|
||||||
|
resource: 'sheet',
|
||||||
|
operation: 'read',
|
||||||
|
documentId: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields').item.json.document }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
sheetName: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields').item.json.sheet }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
filtersUI: {},
|
||||||
|
combineFilters: 'AND',
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||||
|
nodeParameters,
|
||||||
|
) as typeof nodeParameters;
|
||||||
|
expect(resolvedParameters.documentId.value).toHaveProperty('resolvedExpressionValue');
|
||||||
|
expect(resolvedParameters.sheetName.value).toHaveProperty('resolvedExpressionValue');
|
||||||
|
});
|
||||||
|
it('should correctly detect and resolve expressions in a node with resource mapper component', () => {
|
||||||
|
const nodeParameters = {
|
||||||
|
authentication: 'oAuth2',
|
||||||
|
resource: 'sheet',
|
||||||
|
operation: 'read',
|
||||||
|
documentId: {
|
||||||
|
__rl: true,
|
||||||
|
value: '1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc',
|
||||||
|
mode: 'list',
|
||||||
|
cachedResultName: 'Mapping sheet',
|
||||||
|
cachedResultUrl:
|
||||||
|
'https://docs.google.com/spreadsheets/d/1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc/edit?usp=drivesdk',
|
||||||
|
},
|
||||||
|
sheetName: {
|
||||||
|
__rl: true,
|
||||||
|
value: 'gid=0',
|
||||||
|
mode: 'list',
|
||||||
|
cachedResultName: 'Users',
|
||||||
|
cachedResultUrl:
|
||||||
|
'https://docs.google.com/spreadsheets/d/1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc/edit#gid=0',
|
||||||
|
},
|
||||||
|
filtersUI: {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
lookupColumn: 'First name',
|
||||||
|
lookupValue: "={{ $('Edit Fields 1').item.json.userName }}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
combineFilters: 'AND',
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||||
|
nodeParameters,
|
||||||
|
) as typeof nodeParameters;
|
||||||
|
expect(resolvedParameters.filtersUI.values[0].lookupValue).toHaveProperty(
|
||||||
|
'resolvedExpressionValue',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('saveAsNewWorkflow', () => {
|
describe('saveAsNewWorkflow', () => {
|
||||||
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
|
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
|
||||||
const workflow = getDuplicateTestWorkflow();
|
const workflow = getDuplicateTestWorkflow();
|
||||||
|
|
|
@ -693,6 +693,42 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||||
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflowId, node, path, isFullPath);
|
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflowId, node, path, isFullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of provided node parameters with added resolvedExpressionValue
|
||||||
|
* @param nodeParameters
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getNodeParametersWithResolvedExpressions(
|
||||||
|
nodeParameters: INodeParameters,
|
||||||
|
): INodeParameters {
|
||||||
|
function recurse(currentObj: INodeParameters, currentPath: string): INodeParameters {
|
||||||
|
const newObj: INodeParameters = {};
|
||||||
|
for (const key in currentObj) {
|
||||||
|
const value = currentObj[key as keyof typeof currentObj];
|
||||||
|
const path = currentPath ? `${currentPath}.${key}` : key;
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
newObj[key] = recurse(value as INodeParameters, path);
|
||||||
|
} else if (typeof value === 'string' && String(value).startsWith('=')) {
|
||||||
|
// Resolve the expression if it is one
|
||||||
|
let resolved;
|
||||||
|
try {
|
||||||
|
resolved = resolveExpression(value, undefined, { isForCredential: false });
|
||||||
|
} catch (error) {
|
||||||
|
resolved = `Error in expression: "${error.message}"`;
|
||||||
|
}
|
||||||
|
newObj[key] = {
|
||||||
|
value,
|
||||||
|
resolvedExpressionValue: String(resolved),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newObj[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
return recurse(nodeParameters, '');
|
||||||
|
}
|
||||||
|
|
||||||
function resolveExpression(
|
function resolveExpression(
|
||||||
expression: string,
|
expression: string,
|
||||||
siblingParameters: INodeParameters = {},
|
siblingParameters: INodeParameters = {},
|
||||||
|
@ -1159,5 +1195,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||||
getWorkflowProjectRole,
|
getWorkflowProjectRole,
|
||||||
promptSaveUnsavedWorkflowChanges,
|
promptSaveUnsavedWorkflowChanges,
|
||||||
initState,
|
initState,
|
||||||
|
getNodeParametersWithResolvedExpressions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ vi.mock('vue-router', () => ({
|
||||||
name: ENABLED_VIEWS[0],
|
name: ENABLED_VIEWS[0],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
useRouter: vi.fn(),
|
||||||
RouterLink: vi.fn(),
|
RouterLink: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { useRoute } from 'vue-router';
|
||||||
import { useSettingsStore } from './settings.store';
|
import { useSettingsStore } from './settings.store';
|
||||||
import { assert } from '@/utils/assert';
|
import { assert } from '@/utils/assert';
|
||||||
import { useWorkflowsStore } from './workflows.store';
|
import { useWorkflowsStore } from './workflows.store';
|
||||||
import type { ICredentialType, INodeParameters } from 'n8n-workflow';
|
import type { IDataObject, ICredentialType, INodeParameters } from 'n8n-workflow';
|
||||||
import { deepCopy } from 'n8n-workflow';
|
import { deepCopy } from 'n8n-workflow';
|
||||||
import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus';
|
import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus';
|
||||||
import { useNDVStore } from './ndv.store';
|
import { useNDVStore } from './ndv.store';
|
||||||
|
@ -27,7 +27,8 @@ import {
|
||||||
getNodeAuthOptions,
|
getNodeAuthOptions,
|
||||||
getReferencedNodes,
|
getReferencedNodes,
|
||||||
getNodesSchemas,
|
getNodesSchemas,
|
||||||
pruneNodeProperties,
|
processNodeForAssistant,
|
||||||
|
isNodeReferencingInputData,
|
||||||
} from '@/utils/nodeTypesUtils';
|
} from '@/utils/nodeTypesUtils';
|
||||||
import { useNodeTypesStore } from './nodeTypes.store';
|
import { useNodeTypesStore } from './nodeTypes.store';
|
||||||
import { usePostHog } from './posthog.store';
|
import { usePostHog } from './posthog.store';
|
||||||
|
@ -421,6 +422,16 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
const availableAuthOptions = getNodeAuthOptions(nodeType);
|
const availableAuthOptions = getNodeAuthOptions(nodeType);
|
||||||
authType = availableAuthOptions.find((option) => option.value === credentialInUse);
|
authType = availableAuthOptions.find((option) => option.value === credentialInUse);
|
||||||
}
|
}
|
||||||
|
let nodeInputData: { inputNodeName?: string; inputData?: IDataObject } | undefined = undefined;
|
||||||
|
const ndvInput = ndvStore.ndvInputData;
|
||||||
|
if (isNodeReferencingInputData(context.node) && ndvInput?.length) {
|
||||||
|
const inputData = ndvStore.ndvInputData[0].json;
|
||||||
|
const inputNodeName = ndvStore.input.nodeName;
|
||||||
|
nodeInputData = {
|
||||||
|
inputNodeName,
|
||||||
|
inputData,
|
||||||
|
};
|
||||||
|
}
|
||||||
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError'));
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError'));
|
||||||
openChat();
|
openChat();
|
||||||
|
|
||||||
|
@ -435,7 +446,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
firstName: usersStore.currentUser?.firstName ?? '',
|
firstName: usersStore.currentUser?.firstName ?? '',
|
||||||
},
|
},
|
||||||
error: context.error,
|
error: context.error,
|
||||||
node: pruneNodeProperties(context.node, ['position']),
|
node: processNodeForAssistant(context.node, ['position']),
|
||||||
|
nodeInputData,
|
||||||
executionSchema: schemas,
|
executionSchema: schemas,
|
||||||
authType,
|
authType,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Schema } from '@/Interface';
|
import type { Schema } from '@/Interface';
|
||||||
import type { INode, INodeParameters } from 'n8n-workflow';
|
import type { IDataObject, INode, INodeParameters } from 'n8n-workflow';
|
||||||
|
|
||||||
export namespace ChatRequest {
|
export namespace ChatRequest {
|
||||||
interface NodeExecutionSchema {
|
export interface NodeExecutionSchema {
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
schema: Schema;
|
schema: Schema;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ export namespace ChatRequest {
|
||||||
stack?: string;
|
stack?: string;
|
||||||
};
|
};
|
||||||
node: INode;
|
node: INode;
|
||||||
|
nodeInputData?: IDataObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitErrorHelper extends ErrorContext, WorkflowContext {
|
export interface InitErrorHelper extends ErrorContext, WorkflowContext {
|
||||||
|
|
383
packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts
Normal file
383
packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { getReferencedNodes } from '../nodeTypesUtils';
|
||||||
|
import type { INode } from 'n8n-workflow';
|
||||||
|
|
||||||
|
const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: string[] }> = [
|
||||||
|
{
|
||||||
|
caseName: 'Should return an empty array if no referenced nodes',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://httpbin.org/get1',
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should return an array of references for regular node',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
authentication: 'oAuth2',
|
||||||
|
resource: 'sheet',
|
||||||
|
operation: 'read',
|
||||||
|
documentId: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields').item.json.document }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
sheetName: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields 2').item.json.sheet }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
filtersUI: {},
|
||||||
|
combineFilters: 'AND',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.googleSheets',
|
||||||
|
typeVersion: 4.5,
|
||||||
|
position: [440, 0],
|
||||||
|
id: '9a95ad27-06cf-4076-af6b-52846a109a8b',
|
||||||
|
name: 'Google Sheets',
|
||||||
|
credentials: {
|
||||||
|
googleSheetsOAuth2Api: {
|
||||||
|
id: '8QEpi028oHDLXntS',
|
||||||
|
name: 'milorad@n8n.io',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: ['Edit Fields', 'Edit Fields 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should return an array of references for set node',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
mode: 'manual',
|
||||||
|
duplicateItem: false,
|
||||||
|
assignments: {
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: '135e0eb0-f412-430d-8990-731c57cf43ae',
|
||||||
|
name: 'document',
|
||||||
|
value: "={{ $('Edit Fields 2').item.json.document}}",
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
includeOtherFields: false,
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 3.4,
|
||||||
|
position: [560, -140],
|
||||||
|
id: '7306745f-ba8c-451d-ae1a-c627f60fbdd3',
|
||||||
|
name: 'Edit Fields 2',
|
||||||
|
},
|
||||||
|
expected: ['Edit Fields 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should handle expressions with single quotes, double quotes and backticks',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
authentication: 'oAuth2',
|
||||||
|
resource: 'sheet',
|
||||||
|
operation: 'read',
|
||||||
|
documentId: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields').item.json.document }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
sheetName: {
|
||||||
|
__rl: true,
|
||||||
|
value: '={{ $("Edit Fields 2").item.json.sheet }}',
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
rowName: {
|
||||||
|
__rl: true,
|
||||||
|
value: '={{ $(`Edit Fields 3`).item.json.row }}',
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
filtersUI: {},
|
||||||
|
combineFilters: 'AND',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.googleSheets',
|
||||||
|
typeVersion: 4.5,
|
||||||
|
position: [440, 0],
|
||||||
|
id: '9a95ad27-06cf-4076-af6b-52846a109a8b',
|
||||||
|
name: 'Google Sheets',
|
||||||
|
credentials: {
|
||||||
|
googleSheetsOAuth2Api: {
|
||||||
|
id: '8QEpi028oHDLXntS',
|
||||||
|
name: 'milorad@n8n.io',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: ['Edit Fields', 'Edit Fields 2', 'Edit Fields 3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should only add one reference for each referenced node',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
authentication: 'oAuth2',
|
||||||
|
resource: 'sheet',
|
||||||
|
operation: 'read',
|
||||||
|
documentId: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields').item.json.document }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
sheetName: {
|
||||||
|
__rl: true,
|
||||||
|
value: "={{ $('Edit Fields').item.json.sheet }}",
|
||||||
|
mode: 'id',
|
||||||
|
},
|
||||||
|
filtersUI: {},
|
||||||
|
combineFilters: 'AND',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.googleSheets',
|
||||||
|
typeVersion: 4.5,
|
||||||
|
position: [440, 0],
|
||||||
|
id: '9a95ad27-06cf-4076-af6b-52846a109a8b',
|
||||||
|
name: 'Google Sheets',
|
||||||
|
credentials: {
|
||||||
|
googleSheetsOAuth2Api: {
|
||||||
|
id: '8QEpi028oHDLXntS',
|
||||||
|
name: 'milorad@n8n.io',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: ['Edit Fields'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should handle multiple node references in one expression',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: "={{ $('Edit Fields').item.json.one }} {{ $('Edit Fields 2').item.json.two }} {{ $('Edit Fields').item.json.three }}",
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: ['Edit Fields', 'Edit Fields 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should respect whitespace around node references',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: "={{ $(' Edit Fields ').item.json.one }}",
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: [' Edit Fields '],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should ignore whitespace inside expressions',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: "={{ $( 'Edit Fields' ).item.json.one }}",
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: ['Edit Fields'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should ignore special characters in node references',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: "={{ $( 'Ignore ' this' ).item.json.document }",
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should correctly detect node names that contain single quotes',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
// In order to carry over backslashes to test function, the string needs to be double escaped
|
||||||
|
url: "={{ $('Edit \\'Fields\\' 2').item.json.name }}",
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: ["Edit 'Fields' 2"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should correctly detect node names with inner backticks',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: "={{ $('Edit `Fields` 2').item.json.name }}",
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: ['Edit `Fields` 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should correctly detect node names with inner escaped backticks',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
url: '={{ $(`Edit \\`Fields\\` 2`).item.json.name }}',
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: ['Edit `Fields` 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should correctly detect node names with inner escaped double quotes',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
// In order to carry over backslashes to test function, the string needs to be double escaped
|
||||||
|
url: '={{ $("Edit \\"Fields\\" 2").item.json.name }}',
|
||||||
|
authentication: 'none',
|
||||||
|
provideSslCertificates: false,
|
||||||
|
sendQuery: false,
|
||||||
|
sendHeaders: false,
|
||||||
|
sendBody: false,
|
||||||
|
options: {},
|
||||||
|
infoMessage: '',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: ['Edit "Fields" 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: 'Should not detect invalid expressions',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
curlImport: '',
|
||||||
|
method: 'GET',
|
||||||
|
// String not closed properly
|
||||||
|
url: "={{ $('Edit ' fields').item.json.document }",
|
||||||
|
// Mixed quotes
|
||||||
|
url2: '{{ $("Edit \'Fields" 2").item.json.name }}',
|
||||||
|
url3: '{{ $("Edit `Fields" 2").item.json.name }}',
|
||||||
|
// Quotes not escaped
|
||||||
|
url4: '{{ $("Edit "Fields" 2").item.json.name }}',
|
||||||
|
url5: "{{ $('Edit 'Fields' 2').item.json.name }}",
|
||||||
|
url6: '{{ $(`Edit `Fields` 2`).item.json.name }}',
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [220, 220],
|
||||||
|
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
},
|
||||||
|
expected: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => {
|
||||||
|
const caseName = testCase.caseName;
|
||||||
|
it(`${caseName}`, () => {
|
||||||
|
expect(getReferencedNodes(testCase.node)).toEqual(testCase.expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,10 +5,10 @@ import type {
|
||||||
ITemplatesNode,
|
ITemplatesNode,
|
||||||
IVersionNode,
|
IVersionNode,
|
||||||
NodeAuthenticationOption,
|
NodeAuthenticationOption,
|
||||||
Schema,
|
|
||||||
SimplifiedNodeType,
|
SimplifiedNodeType,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { useDataSchema } from '@/composables/useDataSchema';
|
import { useDataSchema } from '@/composables/useDataSchema';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import {
|
import {
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
MAIN_AUTH_FIELD_NAME,
|
MAIN_AUTH_FIELD_NAME,
|
||||||
|
@ -20,20 +20,22 @@ import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import type { ChatRequest } from '@/types/assistant.types';
|
||||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||||
import { isJsonKeyObject } from '@/utils/typesUtils';
|
import { isJsonKeyObject } from '@/utils/typesUtils';
|
||||||
import type {
|
import {
|
||||||
AssignmentCollectionValue,
|
deepCopy,
|
||||||
IDataObject,
|
type IDataObject,
|
||||||
INode,
|
type INode,
|
||||||
INodeCredentialDescription,
|
type INodeCredentialDescription,
|
||||||
INodeExecutionData,
|
type INodeExecutionData,
|
||||||
INodeProperties,
|
type INodeProperties,
|
||||||
INodeTypeDescription,
|
type INodeTypeDescription,
|
||||||
NodeParameterValueType,
|
type NodeParameterValueType,
|
||||||
ResourceMapperField,
|
type ResourceMapperField,
|
||||||
Themed,
|
type Themed,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Constants and utility functions mainly used to get information about
|
Constants and utility functions mainly used to get information about
|
||||||
|
@ -503,9 +505,9 @@ export const getNodeIconColor = (
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Regular expression to extract the node names from the expressions in the template.
|
Regular expression to extract the node names from the expressions in the template.
|
||||||
Example: $(expression) => expression
|
Supports single quotes, double quotes, and backticks.
|
||||||
*/
|
*/
|
||||||
const entityRegex = /\$\((['"])(.*?)\1\)/g;
|
const entityRegex = /\$\(\s*(\\?["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the node names from the expressions in the template.
|
* Extract the node names from the expressions in the template.
|
||||||
|
@ -520,81 +522,89 @@ function extractNodeNames(template: string): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the node names from the expressions in the node parameters.
|
* Unescape quotes in the string. Supports single quotes, double quotes, and backticks.
|
||||||
*/
|
*/
|
||||||
export function getReferencedNodes(node: INode): string[] {
|
export function unescapeQuotes(str: string): string {
|
||||||
const referencedNodes: string[] = [];
|
return str.replace(/\\(['"`])/g, '$1');
|
||||||
if (!node) {
|
|
||||||
return referencedNodes;
|
|
||||||
}
|
|
||||||
// Special case for code node
|
|
||||||
if (node.type === 'n8n-nodes-base.set' && node.parameters.assignments) {
|
|
||||||
const assignments = node.parameters.assignments as AssignmentCollectionValue;
|
|
||||||
if (assignments.assignments?.length) {
|
|
||||||
assignments.assignments.forEach((assignment) => {
|
|
||||||
if (assignment.name && assignment.value && String(assignment.value).startsWith('=')) {
|
|
||||||
const nodeNames = extractNodeNames(String(assignment.value));
|
|
||||||
if (nodeNames.length) {
|
|
||||||
referencedNodes.push(...nodeNames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Object.values(node.parameters).forEach((value) => {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let strValue = String(value);
|
|
||||||
// Handle resource locator
|
|
||||||
if (typeof value === 'object' && 'value' in value) {
|
|
||||||
strValue = String(value.value);
|
|
||||||
}
|
|
||||||
if (strValue.startsWith('=')) {
|
|
||||||
const nodeNames = extractNodeNames(strValue);
|
|
||||||
if (nodeNames.length) {
|
|
||||||
referencedNodes.push(...nodeNames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return referencedNodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove properties from a node based on the provided list of property names.
|
* Extract the node names from the expressions in the node parameters.
|
||||||
* Reruns a new node object with the properties removed.
|
|
||||||
*/
|
*/
|
||||||
export function pruneNodeProperties(node: INode, propsToRemove: string[]): INode {
|
export function getReferencedNodes(node: INode): string[] {
|
||||||
const prunedNode = { ...node };
|
const referencedNodes: Set<string> = new Set();
|
||||||
|
if (!node) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Go through all parameters and check if they contain expressions on any level
|
||||||
|
for (const key in node.parameters) {
|
||||||
|
let names: string[] = [];
|
||||||
|
if (
|
||||||
|
node.parameters[key] &&
|
||||||
|
typeof node.parameters[key] === 'object' &&
|
||||||
|
Object.keys(node.parameters[key]).length
|
||||||
|
) {
|
||||||
|
names = extractNodeNames(JSON.stringify(node.parameters[key]));
|
||||||
|
} else if (typeof node.parameters[key] === 'string' && node.parameters[key]) {
|
||||||
|
names = extractNodeNames(node.parameters[key]);
|
||||||
|
}
|
||||||
|
if (names.length) {
|
||||||
|
names
|
||||||
|
.map((name) => unescapeQuotes(name))
|
||||||
|
.forEach((name) => {
|
||||||
|
referencedNodes.add(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return referencedNodes.size ? Array.from(referencedNodes) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes node object before sending it to AI assistant
|
||||||
|
* - Removes unnecessary properties
|
||||||
|
* - Extracts expressions from the parameters and resolves them
|
||||||
|
* @param node original node object
|
||||||
|
* @param propsToRemove properties to remove from the node object
|
||||||
|
* @returns processed node
|
||||||
|
*/
|
||||||
|
export function processNodeForAssistant(node: INode, propsToRemove: string[]): INode {
|
||||||
|
// Make a copy of the node object so we don't modify the original
|
||||||
|
const nodeForLLM = deepCopy(node);
|
||||||
propsToRemove.forEach((key) => {
|
propsToRemove.forEach((key) => {
|
||||||
delete prunedNode[key as keyof INode];
|
delete nodeForLLM[key as keyof INode];
|
||||||
});
|
});
|
||||||
return prunedNode;
|
const workflowHelpers = useWorkflowHelpers({ router: useRouter() });
|
||||||
|
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||||
|
nodeForLLM.parameters,
|
||||||
|
);
|
||||||
|
nodeForLLM.parameters = resolvedParameters;
|
||||||
|
return nodeForLLM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNodeReferencingInputData(node: INode): boolean {
|
||||||
|
const parametersString = JSON.stringify(node.parameters);
|
||||||
|
const references = ['$json', '$input', '$binary'];
|
||||||
|
return references.some((ref) => parametersString.includes(ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the schema for the referenced nodes as expected by the AI assistant
|
* Get the schema for the referenced nodes as expected by the AI assistant
|
||||||
* @param nodeNames The names of the nodes to get the schema for
|
* @param nodeNames The names of the nodes to get the schema for
|
||||||
* @returns An array of objects containing the node name and the schema
|
* @returns An array of NodeExecutionSchema objects
|
||||||
*/
|
*/
|
||||||
export function getNodesSchemas(nodeNames: string[]) {
|
export function getNodesSchemas(nodeNames: string[]) {
|
||||||
return nodeNames.map((name) => {
|
const schemas: ChatRequest.NodeExecutionSchema[] = [];
|
||||||
|
for (const name of nodeNames) {
|
||||||
const node = useWorkflowsStore().getNodeByName(name);
|
const node = useWorkflowsStore().getNodeByName(name);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return {
|
continue;
|
||||||
nodeName: name,
|
|
||||||
schema: {} as Schema,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
|
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
|
||||||
const schema = getSchemaForExecutionData(
|
const schema = getSchemaForExecutionData(executionDataToJson(getInputDataWithPinned(node)));
|
||||||
executionDataToJson(getInputDataWithPinned(node)),
|
schemas.push({
|
||||||
true,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
schema,
|
schema,
|
||||||
};
|
});
|
||||||
});
|
}
|
||||||
|
return schemas;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue