mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix: Validate custom tool names for forbidden chars (#8878)
This commit is contained in:
parent
4861556a1c
commit
edce632ee6
|
@ -1,6 +1,6 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisibleSelect } from '../utils';
|
||||||
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME } from '../constants';
|
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
|
||||||
import { NDV, WorkflowPage } from '../pages';
|
import { NDV, WorkflowPage } from '../pages';
|
||||||
import { NodeCreator } from '../pages/features/node-creator';
|
import { NodeCreator } from '../pages/features/node-creator';
|
||||||
|
|
||||||
|
@ -485,13 +485,13 @@ describe('NDV', () => {
|
||||||
const connectionGroups = [
|
const connectionGroups = [
|
||||||
{
|
{
|
||||||
title: 'Language Models',
|
title: 'Language Models',
|
||||||
id: 'ai_languageModel'
|
id: 'ai_languageModel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tools',
|
title: 'Tools',
|
||||||
id: 'ai_tool'
|
id: 'ai_tool',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
workflowPage.actions.addInitialNodeToCanvas('AI Agent', { keepNdvOpen: true });
|
workflowPage.actions.addInitialNodeToCanvas('AI Agent', { keepNdvOpen: true });
|
||||||
|
|
||||||
|
@ -513,13 +513,16 @@ describe('NDV', () => {
|
||||||
cy.getByTestId(`add-subnode-${group.id}`).click();
|
cy.getByTestId(`add-subnode-${group.id}`).click();
|
||||||
nodeCreator.getters.getNthCreatorItem(1).click();
|
nodeCreator.getters.getNthCreatorItem(1).click();
|
||||||
getFloatingNodeByPosition('outputSub').click({ force: true });
|
getFloatingNodeByPosition('outputSub').click({ force: true });
|
||||||
cy.getByTestId('subnode-connection-group-ai_tool').findChildByTestId('floating-subnode').should('have.length', 2);
|
cy.getByTestId('subnode-connection-group-ai_tool')
|
||||||
|
.findChildByTestId('floating-subnode')
|
||||||
|
.should('have.length', 2);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since language model has no credentials set, it should show an error
|
// Since language model has no credentials set, it should show an error
|
||||||
cy.get('[class*=hasIssues]').should('have.length', 1);
|
// Sinse code tool require alphanumeric tool name it would also show an error(2 errors, 1 for each tool node)
|
||||||
})
|
cy.get('[class*=hasIssues]').should('have.length', 3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show node name and version in settings', () => {
|
it('should show node name and version in settings', () => {
|
||||||
|
@ -636,14 +639,17 @@ describe('NDV', () => {
|
||||||
it('Should open appropriate node creator after clicking on connection hint link', () => {
|
it('Should open appropriate node creator after clicking on connection hint link', () => {
|
||||||
const nodeCreator = new NodeCreator();
|
const nodeCreator = new NodeCreator();
|
||||||
const hintMapper = {
|
const hintMapper = {
|
||||||
'Memory': 'AI Nodes',
|
Memory: 'AI Nodes',
|
||||||
'Output Parser': 'AI Nodes',
|
'Output Parser': 'AI Nodes',
|
||||||
'Token Splitter': 'Document Loaders',
|
'Token Splitter': 'Document Loaders',
|
||||||
'Tool': 'AI Nodes',
|
Tool: 'AI Nodes',
|
||||||
'Embeddings': 'Vector Stores',
|
Embeddings: 'Vector Stores',
|
||||||
'Vector Store': 'Retrievers'
|
'Vector Store': 'Retrievers',
|
||||||
}
|
};
|
||||||
cy.createFixtureWorkflow('open_node_creator_for_connection.json', `open_node_creator_for_connection ${uuid()}`);
|
cy.createFixtureWorkflow(
|
||||||
|
'open_node_creator_for_connection.json',
|
||||||
|
`open_node_creator_for_connection ${uuid()}`,
|
||||||
|
);
|
||||||
|
|
||||||
Object.entries(hintMapper).forEach(([node, group]) => {
|
Object.entries(hintMapper).forEach(([node, group]) => {
|
||||||
workflowPage.actions.openNode(node);
|
workflowPage.actions.openNode(node);
|
||||||
|
@ -651,5 +657,5 @@ describe('NDV', () => {
|
||||||
nodeCreator.getters.activeSubcategory().should('contain', group);
|
nodeCreator.getters.activeSubcategory().should('contain', group);
|
||||||
cy.realPress('Escape');
|
cy.realPress('Escape');
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ export class ToolCode implements INodeType {
|
||||||
name: 'toolCode',
|
name: 'toolCode',
|
||||||
icon: 'fa:code',
|
icon: 'fa:code',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: [1, 1.1],
|
||||||
description: 'Write a tool in JS or Python',
|
description: 'Write a tool in JS or Python',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Custom Code Tool',
|
name: 'Custom Code Tool',
|
||||||
|
@ -59,6 +59,26 @@ export class ToolCode implements INodeType {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'My_Tool',
|
placeholder: 'My_Tool',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. My_Tool',
|
||||||
|
validateType: 'string-alphanumeric',
|
||||||
|
description:
|
||||||
|
'The name of the function to be called, could contain letters, numbers, and underscores only',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Description',
|
displayName: 'Description',
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class ToolWorkflow implements INodeType {
|
||||||
name: 'toolWorkflow',
|
name: 'toolWorkflow',
|
||||||
icon: 'fa:network-wired',
|
icon: 'fa:network-wired',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: [1, 1.1],
|
||||||
description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.',
|
description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Custom n8n Workflow Tool',
|
name: 'Custom n8n Workflow Tool',
|
||||||
|
@ -62,6 +62,26 @@ export class ToolWorkflow implements INodeType {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'My_Color_Tool',
|
placeholder: 'My_Color_Tool',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. My_Color_Tool',
|
||||||
|
validateType: 'string-alphanumeric',
|
||||||
|
description:
|
||||||
|
'The name of the function to be called, could contain letters, numbers, and underscores only',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Description',
|
displayName: 'Description',
|
||||||
|
|
|
@ -2341,6 +2341,7 @@ export interface ResourceMapperField {
|
||||||
|
|
||||||
export type FieldType =
|
export type FieldType =
|
||||||
| 'string'
|
| 'string'
|
||||||
|
| 'string-alphanumeric'
|
||||||
| 'number'
|
| 'number'
|
||||||
| 'dateTime'
|
| 'dateTime'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
|
|
|
@ -26,7 +26,14 @@ export const tryToParseString = (value: unknown): string => {
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
};
|
};
|
||||||
|
export const tryToParseAlphanumericString = (value: unknown): string => {
|
||||||
|
const parsed = tryToParseString(value);
|
||||||
|
const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||||
|
if (!regex.test(parsed)) {
|
||||||
|
throw new ApplicationError('Value is not a valid alphanumeric string', { extra: { value } });
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
export const tryToParseBoolean = (value: unknown): value is boolean => {
|
export const tryToParseBoolean = (value: unknown): value is boolean => {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
|
@ -180,6 +187,17 @@ export const validateFieldType = (
|
||||||
return { valid: false, errorMessage: defaultErrorMessage };
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'string-alphanumeric': {
|
||||||
|
try {
|
||||||
|
return { valid: true, newValue: tryToParseAlphanumericString(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorMessage:
|
||||||
|
'Value is not a valid alphanumeric string, only letters, numbers and underscore allowed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'number': {
|
case 'number': {
|
||||||
try {
|
try {
|
||||||
if (strict && typeof value !== 'number') {
|
if (strict && typeof value !== 'number') {
|
||||||
|
|
Loading…
Reference in a new issue