mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
fix: Fix node graph telemetry with default values (#8297)
This commit is contained in:
parent
dcc76f3480
commit
93b969a327
|
@ -1,3 +1,4 @@
|
|||
import { getNodeParameters } from './NodeHelpers';
|
||||
import type {
|
||||
IConnection,
|
||||
INode,
|
||||
|
@ -7,7 +8,6 @@ import type {
|
|||
INodesGraphResult,
|
||||
IWorkflowBase,
|
||||
INodeTypes,
|
||||
INodeType,
|
||||
} from './Interfaces';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
|
@ -21,28 +21,6 @@ export function isNumber(value: unknown): value is number {
|
|||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
function getStickyDimensions(note: INode, stickyType: INodeType | undefined) {
|
||||
const heightProperty = stickyType?.description?.properties.find(
|
||||
(property) => property.name === 'height',
|
||||
);
|
||||
const widthProperty = stickyType?.description?.properties.find(
|
||||
(property) => property.name === 'width',
|
||||
);
|
||||
|
||||
const defaultHeight =
|
||||
heightProperty && isNumber(heightProperty?.default) ? heightProperty.default : 0;
|
||||
const defaultWidth =
|
||||
widthProperty && isNumber(widthProperty?.default) ? widthProperty.default : 0;
|
||||
|
||||
const height: number = isNumber(note.parameters.height) ? note.parameters.height : defaultHeight;
|
||||
const width: number = isNumber(note.parameters.width) ? note.parameters.width : defaultWidth;
|
||||
|
||||
return {
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
type XYPosition = [number, number];
|
||||
|
||||
function areOverlapping(
|
||||
|
@ -112,123 +90,152 @@ export function getDomainPath(raw: string, urlParts = URL_PARTS_REGEX): string {
|
|||
}
|
||||
|
||||
export function generateNodesGraph(
|
||||
workflow: IWorkflowBase,
|
||||
workflow: Partial<IWorkflowBase>,
|
||||
nodeTypes: INodeTypes,
|
||||
options?: {
|
||||
sourceInstanceId?: string;
|
||||
nodeIdMap?: { [curr: string]: string };
|
||||
},
|
||||
): INodesGraphResult {
|
||||
const nodesGraph: INodesGraph = {
|
||||
const nodeGraph: INodesGraph = {
|
||||
node_types: [],
|
||||
node_connections: [],
|
||||
nodes: {},
|
||||
notes: {},
|
||||
is_pinned: Object.keys(workflow.pinData ?? {}).length > 0,
|
||||
};
|
||||
const nodeNameAndIndex: INodeNameIndex = {};
|
||||
const nameIndices: INodeNameIndex = {};
|
||||
const webhookNodeNames: string[] = [];
|
||||
|
||||
try {
|
||||
const notes = workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE);
|
||||
const otherNodes = workflow.nodes.filter((node) => node.type !== STICKY_NODE_TYPE);
|
||||
const notes = (workflow.nodes ?? []).filter((node) => node.type === STICKY_NODE_TYPE);
|
||||
const otherNodes = (workflow.nodes ?? []).filter((node) => node.type !== STICKY_NODE_TYPE);
|
||||
|
||||
notes.forEach((stickyNote: INode, index: number) => {
|
||||
const stickyType = nodeTypes.getByNameAndVersion(STICKY_NODE_TYPE, stickyNote.typeVersion);
|
||||
const { height, width } = getStickyDimensions(stickyNote, stickyType);
|
||||
notes.forEach((stickyNote: INode, index: number) => {
|
||||
const stickyType = nodeTypes.getByNameAndVersion(STICKY_NODE_TYPE, stickyNote.typeVersion);
|
||||
if (!stickyType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topLeft = stickyNote.position;
|
||||
const bottomRight: [number, number] = [topLeft[0] + width, topLeft[1] + height];
|
||||
const overlapping = Boolean(
|
||||
otherNodes.find((node) => areOverlapping(topLeft, bottomRight, node.position)),
|
||||
);
|
||||
nodesGraph.notes[index] = {
|
||||
overlapping,
|
||||
position: topLeft,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
});
|
||||
const nodeParameters =
|
||||
getNodeParameters(
|
||||
stickyType.description.properties,
|
||||
stickyNote.parameters,
|
||||
true,
|
||||
false,
|
||||
stickyNote,
|
||||
) ?? {};
|
||||
|
||||
otherNodes.forEach((node: INode, index: number) => {
|
||||
nodesGraph.node_types.push(node.type);
|
||||
const nodeItem: INodeGraphItem = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
version: node.typeVersion,
|
||||
position: node.position,
|
||||
};
|
||||
const height: number = typeof nodeParameters.height === 'number' ? nodeParameters.height : 0;
|
||||
const width: number = typeof nodeParameters.width === 'number' ? nodeParameters.width : 0;
|
||||
|
||||
if (options?.sourceInstanceId) {
|
||||
nodeItem.src_instance_id = options.sourceInstanceId;
|
||||
}
|
||||
const topLeft = stickyNote.position;
|
||||
const bottomRight: [number, number] = [topLeft[0] + width, topLeft[1] + height];
|
||||
const overlapping = Boolean(
|
||||
otherNodes.find((node) => areOverlapping(topLeft, bottomRight, node.position)),
|
||||
);
|
||||
nodeGraph.notes[index] = {
|
||||
overlapping,
|
||||
position: topLeft,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
});
|
||||
|
||||
if (node.id && options?.nodeIdMap?.[node.id]) {
|
||||
nodeItem.src_node_id = options.nodeIdMap[node.id];
|
||||
}
|
||||
|
||||
if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 1) {
|
||||
try {
|
||||
nodeItem.domain = new URL(node.parameters.url as string).hostname;
|
||||
} catch {
|
||||
nodeItem.domain = getDomainBase(node.parameters.url as string);
|
||||
}
|
||||
} else if (node.type === 'n8n-nodes-base.httpRequest' && [2, 3].includes(node.typeVersion)) {
|
||||
const { authentication } = node.parameters as { authentication: string };
|
||||
|
||||
nodeItem.credential_type = {
|
||||
none: 'none',
|
||||
genericCredentialType: node.parameters.genericAuthType as string,
|
||||
predefinedCredentialType: node.parameters.nodeCredentialType as string,
|
||||
}[authentication];
|
||||
|
||||
nodeItem.credential_set = node.credentials
|
||||
? Object.keys(node.credentials).length > 0
|
||||
: false;
|
||||
|
||||
const { url } = node.parameters as { url: string };
|
||||
|
||||
nodeItem.domain_base = getDomainBase(url);
|
||||
nodeItem.domain_path = getDomainPath(url);
|
||||
nodeItem.method = node.parameters.requestMethod as string;
|
||||
} else if (node.type === 'n8n-nodes-base.webhook') {
|
||||
webhookNodeNames.push(node.name);
|
||||
} else {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(node.type);
|
||||
|
||||
nodeType?.description?.properties?.forEach((property) => {
|
||||
if (
|
||||
property.name === 'operation' ||
|
||||
property.name === 'resource' ||
|
||||
property.name === 'mode'
|
||||
) {
|
||||
nodeItem[property.name] = property.default ? property.default.toString() : undefined;
|
||||
}
|
||||
});
|
||||
|
||||
nodeItem.operation = node.parameters.operation?.toString() ?? nodeItem.operation;
|
||||
nodeItem.resource = node.parameters.resource?.toString() ?? nodeItem.resource;
|
||||
nodeItem.mode = node.parameters.mode?.toString() ?? nodeItem.mode;
|
||||
}
|
||||
nodesGraph.nodes[`${index}`] = nodeItem;
|
||||
nodeNameAndIndex[node.name] = index.toString();
|
||||
});
|
||||
|
||||
const getGraphConnectionItem = (startNode: string, connectionItem: IConnection) => {
|
||||
return { start: nodeNameAndIndex[startNode], end: nodeNameAndIndex[connectionItem.node] };
|
||||
otherNodes.forEach((node: INode, index: number) => {
|
||||
nodeGraph.node_types.push(node.type);
|
||||
const nodeItem: INodeGraphItem = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
version: node.typeVersion,
|
||||
position: node.position,
|
||||
};
|
||||
|
||||
Object.keys(workflow.connections).forEach((nodeName) => {
|
||||
const connections = workflow.connections[nodeName];
|
||||
connections.main.forEach((element) => {
|
||||
if (options?.sourceInstanceId) {
|
||||
nodeItem.src_instance_id = options.sourceInstanceId;
|
||||
}
|
||||
|
||||
if (node.id && options?.nodeIdMap?.[node.id]) {
|
||||
nodeItem.src_node_id = options.nodeIdMap[node.id];
|
||||
}
|
||||
|
||||
if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 1) {
|
||||
try {
|
||||
nodeItem.domain = new URL(node.parameters.url as string).hostname;
|
||||
} catch {
|
||||
nodeItem.domain = getDomainBase(node.parameters.url as string);
|
||||
}
|
||||
} else if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion > 1) {
|
||||
const { authentication } = node.parameters as { authentication: string };
|
||||
|
||||
nodeItem.credential_type = {
|
||||
none: 'none',
|
||||
genericCredentialType: node.parameters.genericAuthType as string,
|
||||
predefinedCredentialType: node.parameters.nodeCredentialType as string,
|
||||
}[authentication];
|
||||
|
||||
nodeItem.credential_set = node.credentials ? Object.keys(node.credentials).length > 0 : false;
|
||||
|
||||
const { url } = node.parameters as { url: string };
|
||||
|
||||
nodeItem.domain_base = getDomainBase(url);
|
||||
nodeItem.domain_path = getDomainPath(url);
|
||||
nodeItem.method = node.parameters.requestMethod as string;
|
||||
} else if (node.type === 'n8n-nodes-base.webhook') {
|
||||
webhookNodeNames.push(node.name);
|
||||
} else {
|
||||
try {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(node.type);
|
||||
if (nodeType) {
|
||||
const nodeParameters = getNodeParameters(
|
||||
nodeType.description.properties,
|
||||
node.parameters,
|
||||
true,
|
||||
false,
|
||||
node,
|
||||
);
|
||||
|
||||
if (nodeParameters) {
|
||||
const keys: Array<'operation' | 'resource' | 'mode'> = [
|
||||
'operation',
|
||||
'resource',
|
||||
'mode',
|
||||
];
|
||||
keys.forEach((key) => {
|
||||
if (nodeParameters.hasOwnProperty(key)) {
|
||||
nodeItem[key] = nodeParameters[key]?.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (!(e instanceof Error && e.message.includes('Unrecognized node type'))) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodeGraph.nodes[index.toString()] = nodeItem;
|
||||
nameIndices[node.name] = index.toString();
|
||||
});
|
||||
|
||||
const getGraphConnectionItem = (startNode: string, connectionItem: IConnection) => {
|
||||
return { start: nameIndices[startNode], end: nameIndices[connectionItem.node] };
|
||||
};
|
||||
|
||||
Object.keys(workflow.connections ?? []).forEach((nodeName) => {
|
||||
const connections = workflow.connections?.[nodeName];
|
||||
if (!connections) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(connections).forEach((key) => {
|
||||
connections[key].forEach((element) => {
|
||||
element.forEach((element2) => {
|
||||
nodesGraph.node_connections.push(getGraphConnectionItem(nodeName, element2));
|
||||
nodeGraph.node_connections.push(getGraphConnectionItem(nodeName, element2));
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames };
|
||||
}
|
||||
});
|
||||
|
||||
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex, webhookNodeNames };
|
||||
return { nodeGraph, nameIndices, webhookNodeNames };
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ import type {
|
|||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
IVersionedNodeType,
|
||||
IRunExecutionData,
|
||||
ITaskDataConnections,
|
||||
IWorkflowBase,
|
||||
|
@ -40,6 +37,7 @@ import * as NodeHelpers from '@/NodeHelpers';
|
|||
import { deepCopy } from '@/utils';
|
||||
import { getGlobalState } from '@/GlobalState';
|
||||
import { ApplicationError } from '@/errors/application.error';
|
||||
import { NodeTypes as NodeTypesClass } from './NodeTypes';
|
||||
|
||||
export interface INodeTypesObject {
|
||||
[key: string]: INodeType;
|
||||
|
@ -528,135 +526,6 @@ export function getExecuteSingleFunctions(
|
|||
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
|
||||
}
|
||||
|
||||
class NodeTypesClass implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {
|
||||
'test.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.setMulti': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set Multi',
|
||||
name: 'setMulti',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets multiple values',
|
||||
defaults: {
|
||||
name: 'Set Multi',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
placeholder: 'Name of the property to write data to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'The string value to write in the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.switch': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Switches',
|
||||
defaults: {
|
||||
name: 'Switch',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main', 'main', 'main', 'main'],
|
||||
outputNames: ['0', '1', '2', '3'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return this.nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
let nodeTypesInstance: NodeTypesClass | undefined;
|
||||
|
||||
export function NodeTypes(): NodeTypesClass {
|
||||
|
|
670
packages/workflow/test/NodeTypes.ts
Normal file
670
packages/workflow/test/NodeTypes.ts
Normal file
|
@ -0,0 +1,670 @@
|
|||
import {
|
||||
NodeHelpers,
|
||||
type INodeType,
|
||||
type INodeTypeData,
|
||||
type INodeTypes,
|
||||
type IVersionedNodeType,
|
||||
type LoadedClass,
|
||||
} from '@/index';
|
||||
|
||||
const stickyNode: LoadedClass<INodeType> = {
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Sticky Note',
|
||||
name: 'n8n-nodes-base.stickyNote',
|
||||
icon: 'fa:sticky-note',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Make your workflow easier to understand',
|
||||
defaults: { name: 'Sticky Note', color: '#FFD233' },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default:
|
||||
"## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)",
|
||||
},
|
||||
{ displayName: 'Height', name: 'height', type: 'number', required: true, default: 160 },
|
||||
{ displayName: 'Width', name: 'width', type: 'number', required: true, default: 240 },
|
||||
{ displayName: 'Color', name: 'color', type: 'number', required: true, default: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
sourcePath: '',
|
||||
};
|
||||
|
||||
const googleSheetsNode: LoadedClass<IVersionedNodeType> = {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
nodeVersions: {
|
||||
'1': {
|
||||
methods: {
|
||||
loadOptions: {},
|
||||
credentialTest: {},
|
||||
},
|
||||
description: {
|
||||
displayName: 'Google Sheets ',
|
||||
name: 'googleSheets',
|
||||
icon: 'file:googleSheets.svg',
|
||||
group: ['input', 'output'],
|
||||
defaultVersion: 4.2,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Read, update and write data to Google Sheets',
|
||||
version: [1, 2],
|
||||
defaults: {
|
||||
name: 'Google Sheets',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['serviceAccount'],
|
||||
},
|
||||
},
|
||||
testedBy: 'googleApiCredentialTest',
|
||||
},
|
||||
{
|
||||
name: 'googleSheetsOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'<strong>New node version available:</strong> get the latest version with added features from the nodes panel.',
|
||||
name: 'oldVersionNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Service Account',
|
||||
value: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'serviceAccount',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'OAuth2 (recommended)',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
{
|
||||
name: 'Service Account',
|
||||
value: 'serviceAccount',
|
||||
},
|
||||
],
|
||||
default: 'oAuth2',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Spreadsheet',
|
||||
value: 'spreadsheet',
|
||||
},
|
||||
{
|
||||
name: 'Sheet',
|
||||
value: 'sheet',
|
||||
},
|
||||
],
|
||||
default: 'sheet',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
description: 'Append data to a sheet',
|
||||
action: 'Append data to a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Clear',
|
||||
value: 'clear',
|
||||
description: 'Clear data from a sheet',
|
||||
action: 'Clear a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new sheet',
|
||||
action: 'Create a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Create or Update',
|
||||
value: 'upsert',
|
||||
description:
|
||||
'Create a new record, or update the current one if it already exists (upsert)',
|
||||
action: 'Create or update a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete columns and rows from a sheet',
|
||||
action: 'Delete a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Lookup',
|
||||
value: 'lookup',
|
||||
description: 'Look up a specific column value and return the matching row',
|
||||
action: 'Look up a column value in a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Read',
|
||||
value: 'read',
|
||||
description: 'Read data from a sheet',
|
||||
action: 'Read a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
description: 'Remove a sheet',
|
||||
action: 'Remove a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update rows in a sheet',
|
||||
action: 'Update a sheet',
|
||||
},
|
||||
],
|
||||
default: 'read',
|
||||
},
|
||||
{
|
||||
displayName: 'Spreadsheet ID',
|
||||
name: 'sheetId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'The ID of the Google Spreadsheet. Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/.',
|
||||
},
|
||||
{
|
||||
displayName: 'Range',
|
||||
name: 'range',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
hide: {
|
||||
operation: ['create', 'delete', 'remove'],
|
||||
},
|
||||
},
|
||||
default: 'A:F',
|
||||
required: true,
|
||||
description:
|
||||
'The table range to read from or to append data to. See the Google <a href="https://developers.google.com/sheets/api/guides/values#writing">documentation</a> for the details. If it contains multiple sheets it can also be added like this: "MySheet!A:F"',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'4': {
|
||||
description: {
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
version: [3, 4, 4.1, 4.2],
|
||||
credentials: [
|
||||
{
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['serviceAccount'],
|
||||
},
|
||||
},
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
testedBy: 'googleApiCredentialTest',
|
||||
},
|
||||
{
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
name: 'googleSheetsOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
defaults: {
|
||||
name: 'Google Sheets',
|
||||
},
|
||||
defaultVersion: 4.2,
|
||||
description: 'Read, update and write data to Google Sheets',
|
||||
displayName: 'Google Sheets',
|
||||
group: ['input', 'output'],
|
||||
icon: 'file:googleSheets.svg',
|
||||
inputs: ['main'],
|
||||
name: 'googleSheets',
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
default: 'oAuth2',
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
options: [
|
||||
{
|
||||
name: 'Service Account',
|
||||
value: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2 (recommended)',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
type: 'options',
|
||||
},
|
||||
{
|
||||
default: 'sheet',
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Document',
|
||||
value: 'spreadsheet',
|
||||
},
|
||||
{
|
||||
name: 'Sheet Within Document',
|
||||
value: 'sheet',
|
||||
},
|
||||
],
|
||||
type: 'options',
|
||||
},
|
||||
{
|
||||
default: 'read',
|
||||
displayName: 'Operation',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
name: 'operation',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
action: 'Append or update row in sheet',
|
||||
description: 'Append a new row or update an existing one (upsert)',
|
||||
name: 'Append or Update Row',
|
||||
value: 'appendOrUpdate',
|
||||
},
|
||||
{
|
||||
action: 'Append row in sheet',
|
||||
description: 'Create a new row in a sheet',
|
||||
name: 'Append Row',
|
||||
value: 'append',
|
||||
},
|
||||
{
|
||||
action: 'Clear sheet',
|
||||
description: 'Delete all the contents or a part of a sheet',
|
||||
name: 'Clear',
|
||||
value: 'clear',
|
||||
},
|
||||
{
|
||||
action: 'Create sheet',
|
||||
description: 'Create a new sheet',
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
},
|
||||
{
|
||||
action: 'Delete sheet',
|
||||
description: 'Permanently delete a sheet',
|
||||
name: 'Delete',
|
||||
value: 'remove',
|
||||
},
|
||||
{
|
||||
action: 'Delete rows or columns from sheet',
|
||||
description: 'Delete columns or rows from a sheet',
|
||||
name: 'Delete Rows or Columns',
|
||||
value: 'delete',
|
||||
},
|
||||
{
|
||||
action: 'Get row(s) in sheet',
|
||||
description: 'Retrieve one or more rows from a sheet',
|
||||
name: 'Get Row(s)',
|
||||
value: 'read',
|
||||
},
|
||||
{
|
||||
action: 'Update row in sheet',
|
||||
description: 'Update an existing row in a sheet',
|
||||
name: 'Update Row',
|
||||
value: 'update',
|
||||
},
|
||||
],
|
||||
type: 'options',
|
||||
},
|
||||
{
|
||||
default: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
},
|
||||
displayName: 'Document',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchable: true,
|
||||
searchListMethod: 'spreadSheetsSearch',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
extractValue: {
|
||||
regex:
|
||||
'https:\\/\\/(?:drive|docs)\\.google\\.com(?:\\/.*|)\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
|
||||
type: 'regex',
|
||||
},
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
properties: {
|
||||
errorMessage: 'Not a valid Google Drive File URL',
|
||||
regex:
|
||||
'https:\\/\\/(?:drive|docs)\\.google\\.com(?:\\/.*|)\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
|
||||
},
|
||||
type: 'regex',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
url: '=https://docs.google.com/spreadsheets/d/{{$value}}/edit',
|
||||
validation: [
|
||||
{
|
||||
properties: {
|
||||
errorMessage: 'Not a valid Google Drive File ID',
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
},
|
||||
type: 'regex',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'documentId',
|
||||
required: true,
|
||||
type: 'resourceLocator',
|
||||
},
|
||||
{
|
||||
default: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
},
|
||||
displayName: 'Sheet',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'append',
|
||||
'appendOrUpdate',
|
||||
'clear',
|
||||
'delete',
|
||||
'read',
|
||||
'remove',
|
||||
'update',
|
||||
],
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchable: false,
|
||||
searchListMethod: 'sheetsSearch',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
extractValue: {
|
||||
regex:
|
||||
'https:\\/\\/docs\\.google\\.com/spreadsheets\\/d\\/[0-9a-zA-Z\\-_]+\\/edit\\#gid=([0-9]+)',
|
||||
type: 'regex',
|
||||
},
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
properties: {
|
||||
errorMessage: 'Not a valid Sheet URL',
|
||||
regex:
|
||||
'https:\\/\\/docs\\.google\\.com/spreadsheets\\/d\\/[0-9a-zA-Z\\-_]+\\/edit\\#gid=([0-9]+)',
|
||||
},
|
||||
type: 'regex',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
properties: {
|
||||
errorMessage: 'Not a valid Sheet ID',
|
||||
regex: '((gid=)?[0-9]{1,})',
|
||||
},
|
||||
type: 'regex',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'sheetName',
|
||||
required: true,
|
||||
type: 'resourceLocator',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['documentId.value'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
currentVersion: 4.2,
|
||||
description: {
|
||||
displayName: 'Google Sheets',
|
||||
name: 'googleSheets',
|
||||
icon: 'file:googleSheets.svg',
|
||||
group: ['input', 'output'],
|
||||
defaultVersion: 4.2,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Read, update and write data to Google Sheets',
|
||||
},
|
||||
getNodeType(version: number | undefined) {
|
||||
return this.nodeVersions[Math.floor(version ?? this.currentVersion)];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class NodeTypes implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {
|
||||
'n8n-nodes-base.stickyNote': stickyNode,
|
||||
'test.googleSheets': googleSheetsNode,
|
||||
'test.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.setMulti': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set Multi',
|
||||
name: 'setMulti',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets multiple values',
|
||||
defaults: {
|
||||
name: 'Set Multi',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
placeholder: 'Name of the property to write data to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'The string value to write in the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.switch': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Switches',
|
||||
defaults: {
|
||||
name: 'Switch',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main', 'main', 'main', 'main'],
|
||||
outputNames: ['0', '1', '2', '3'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return this.nodeTypes[nodeType]?.type;
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
if (this.nodeTypes[nodeType]?.type) {
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType]?.type, version);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,12 @@
|
|||
import { v5 as uuidv5, v3 as uuidv3, v4 as uuidv4, v1 as uuidv1 } from 'uuid';
|
||||
import { ANONYMIZATION_CHARACTER as CHAR, getDomainBase, getDomainPath } from '@/TelemetryHelpers';
|
||||
import {
|
||||
ANONYMIZATION_CHARACTER as CHAR,
|
||||
generateNodesGraph,
|
||||
getDomainBase,
|
||||
getDomainPath,
|
||||
} from '@/TelemetryHelpers';
|
||||
import type { IWorkflowBase } from '@/index';
|
||||
import { nodeTypes } from './ExpressionExtensions/Helpers';
|
||||
|
||||
describe('getDomainBase should return protocol plus domain', () => {
|
||||
test('in valid URLs', () => {
|
||||
|
@ -67,6 +74,697 @@ describe('getDomainPath should return pathname, excluding query string', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('generateNodesGraph', () => {
|
||||
test('should return node graph when node type is unknown', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
createdAt: new Date('2024-01-05T13:49:14.244Z'),
|
||||
updatedAt: new Date('2024-01-05T15:44:31.000Z'),
|
||||
id: 'NfV4GV9aQTifSLc2',
|
||||
name: 'My workflow 26',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
name: 'When clicking "Execute Workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
documentId: { __rl: true, mode: 'list', value: '' },
|
||||
sheetName: { __rl: true, mode: 'list', value: '' },
|
||||
},
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
name: 'Google Sheets',
|
||||
type: 'test.unknown',
|
||||
typeVersion: 4.2,
|
||||
position: [640, 420],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking "Execute Workflow"': {
|
||||
main: [[{ node: 'Google Sheets', type: 'main', index: 0 }]],
|
||||
},
|
||||
},
|
||||
settings: { executionOrder: 'v1' },
|
||||
pinData: {},
|
||||
versionId: '70b92d94-0e9a-4b41-9976-a654df420af5',
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.manualTrigger', 'test.unknown'],
|
||||
node_connections: [{ start: '0', end: '1' }],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
'1': {
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
type: 'test.unknown',
|
||||
version: 4.2,
|
||||
position: [640, 420],
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'When clicking "Execute Workflow"': '0', 'Google Sheets': '1' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return node graph when workflow is empty', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
createdAt: new Date('2024-01-05T13:49:14.244Z'),
|
||||
updatedAt: new Date('2024-01-05T15:44:31.000Z'),
|
||||
id: 'NfV4GV9aQTifSLc2',
|
||||
name: 'My workflow 26',
|
||||
active: false,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
settings: { executionOrder: 'v1' },
|
||||
pinData: {},
|
||||
versionId: '70b92d94-0e9a-4b41-9976-a654df420af5',
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: [],
|
||||
node_connections: [],
|
||||
nodes: {},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: {},
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return node graph when workflow keys are not set', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: [],
|
||||
node_connections: [],
|
||||
nodes: {},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: {},
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return node graph when node has multiple operation fields with different display options', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
createdAt: new Date('2024-01-05T13:49:14.244Z'),
|
||||
updatedAt: new Date('2024-01-05T15:44:31.000Z'),
|
||||
id: 'NfV4GV9aQTifSLc2',
|
||||
name: 'My workflow 26',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
name: 'When clicking "Execute Workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
documentId: { __rl: true, mode: 'list', value: '' },
|
||||
sheetName: { __rl: true, mode: 'list', value: '' },
|
||||
},
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
name: 'Google Sheets',
|
||||
type: 'test.googleSheets',
|
||||
typeVersion: 4.2,
|
||||
position: [640, 420],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking "Execute Workflow"': {
|
||||
main: [[{ node: 'Google Sheets', type: 'main', index: 0 }]],
|
||||
},
|
||||
},
|
||||
settings: { executionOrder: 'v1' },
|
||||
pinData: {},
|
||||
versionId: '70b92d94-0e9a-4b41-9976-a654df420af5',
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.manualTrigger', 'test.googleSheets'],
|
||||
node_connections: [{ start: '0', end: '1' }],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
'1': {
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
type: 'test.googleSheets',
|
||||
version: 4.2,
|
||||
position: [640, 420],
|
||||
operation: 'read',
|
||||
resource: 'sheet',
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'When clicking "Execute Workflow"': '0', 'Google Sheets': '1' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return node graph with stickies of default size', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
createdAt: new Date('2024-01-05T13:49:14.244Z'),
|
||||
updatedAt: new Date('2024-01-05T15:44:31.000Z'),
|
||||
id: 'NfV4GV9aQTifSLc2',
|
||||
name: 'My workflow 26',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
name: 'When clicking "Execute Workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
documentId: { __rl: true, mode: 'list', value: '' },
|
||||
sheetName: { __rl: true, mode: 'list', value: '' },
|
||||
},
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
name: 'Google Sheets',
|
||||
type: 'test.googleSheets',
|
||||
typeVersion: 4.2,
|
||||
position: [640, 420],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
content:
|
||||
"test\n\n## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)",
|
||||
},
|
||||
id: '03e85c3e-4303-4f93-8d62-e05d457e8f70',
|
||||
name: 'Sticky Note',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [240, 140],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking "Execute Workflow"': {
|
||||
main: [[{ node: 'Google Sheets', type: 'main', index: 0 }]],
|
||||
},
|
||||
},
|
||||
settings: { executionOrder: 'v1' },
|
||||
pinData: {},
|
||||
versionId: '70b92d94-0e9a-4b41-9976-a654df420af5',
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.manualTrigger', 'test.googleSheets'],
|
||||
node_connections: [{ start: '0', end: '1' }],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
'1': {
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
type: 'test.googleSheets',
|
||||
version: 4.2,
|
||||
position: [640, 420],
|
||||
operation: 'read',
|
||||
resource: 'sheet',
|
||||
},
|
||||
},
|
||||
notes: { '0': { overlapping: false, position: [240, 140], height: 160, width: 240 } },
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'When clicking "Execute Workflow"': '0', 'Google Sheets': '1' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return node graph with stickies indicating overlap', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
createdAt: new Date('2024-01-05T13:49:14.244Z'),
|
||||
updatedAt: new Date('2024-01-05T15:44:31.000Z'),
|
||||
id: 'NfV4GV9aQTifSLc2',
|
||||
name: 'My workflow 26',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
name: 'When clicking "Execute Workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
documentId: { __rl: true, mode: 'list', value: '' },
|
||||
sheetName: { __rl: true, mode: 'list', value: '' },
|
||||
},
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
name: 'Google Sheets',
|
||||
type: 'test.googleSheets',
|
||||
typeVersion: 4.2,
|
||||
position: [640, 420],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
content:
|
||||
"test\n\n## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)",
|
||||
height: 488,
|
||||
width: 645,
|
||||
},
|
||||
id: '03e85c3e-4303-4f93-8d62-e05d457e8f70',
|
||||
name: 'Sticky Note',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [240, 140],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking "Execute Workflow"': {
|
||||
main: [[{ node: 'Google Sheets', type: 'main', index: 0 }]],
|
||||
},
|
||||
},
|
||||
settings: { executionOrder: 'v1' },
|
||||
pinData: {},
|
||||
versionId: '70b92d94-0e9a-4b41-9976-a654df420af5',
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.manualTrigger', 'test.googleSheets'],
|
||||
node_connections: [{ start: '0', end: '1' }],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'fa7d5628-5a47-4c8f-98ef-fb3532e5a9f5',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
position: [420, 420],
|
||||
},
|
||||
'1': {
|
||||
id: '266128b9-e5db-4c26-9555-185d48946afb',
|
||||
type: 'test.googleSheets',
|
||||
version: 4.2,
|
||||
position: [640, 420],
|
||||
operation: 'read',
|
||||
resource: 'sheet',
|
||||
},
|
||||
},
|
||||
notes: { '0': { overlapping: true, position: [240, 140], height: 488, width: 645 } },
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'When clicking "Execute Workflow"': '0', 'Google Sheets': '1' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return node graph indicating pinned data', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'e59d3ad9-3448-4899-9f47-d2922c8727ce',
|
||||
name: 'When clicking "Execute Workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [460, 460],
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
pinData: {
|
||||
'When clicking "Execute Workflow"': [],
|
||||
},
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nameIndices: {
|
||||
'When clicking "Execute Workflow"': '0',
|
||||
},
|
||||
nodeGraph: {
|
||||
is_pinned: true,
|
||||
node_connections: [],
|
||||
node_types: ['n8n-nodes-base.manualTrigger'],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'e59d3ad9-3448-4899-9f47-d2922c8727ce',
|
||||
position: [460, 460],
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
},
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return graph with webhook node', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
path: 'bf4c0699-cff8-4440-8964-8e97fda8b4f8',
|
||||
options: {},
|
||||
},
|
||||
id: '5e49e129-2c59-4650-95ea-14d4b94db1f3',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1.1,
|
||||
position: [520, 380],
|
||||
webhookId: 'bf4c0699-cff8-4440-8964-8e97fda8b4f8',
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
pinData: {},
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.webhook'],
|
||||
node_connections: [],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: '5e49e129-2c59-4650-95ea-14d4b94db1f3',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
version: 1.1,
|
||||
position: [520, 380],
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { Webhook: '0' },
|
||||
webhookNodeNames: ['Webhook'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return graph with http v4 node with generic auth', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
url: 'google.com/path/test',
|
||||
authentication: 'genericCredentialType',
|
||||
genericAuthType: 'httpBasicAuth',
|
||||
options: {},
|
||||
},
|
||||
id: '04d6e44f-09c1-454d-9225-60aeed7f022c',
|
||||
name: 'HTTP Request V4 with generic auth',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.1,
|
||||
position: [780, 120],
|
||||
credentials: {
|
||||
httpBasicAuth: {
|
||||
id: 'yuuJAO2Ang5B64wd',
|
||||
name: 'Unnamed credential',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
pinData: {},
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.httpRequest'],
|
||||
node_connections: [],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: '04d6e44f-09c1-454d-9225-60aeed7f022c',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.1,
|
||||
position: [780, 120],
|
||||
credential_type: 'httpBasicAuth',
|
||||
credential_set: true,
|
||||
domain_base: 'google.com',
|
||||
domain_path: '/path/test',
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'HTTP Request V4 with generic auth': '0' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return graph with HTTP V4 with predefined cred', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
url: 'google.com/path/test',
|
||||
authentication: 'predefinedCredentialType',
|
||||
nodeCredentialType: 'activeCampaignApi',
|
||||
options: {},
|
||||
},
|
||||
id: 'dcc4a9e1-c2c5-4d7e-aec0-2a23adabbb77',
|
||||
name: 'HTTP Request V4 with predefined cred',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.1,
|
||||
position: [320, 220],
|
||||
credentials: {
|
||||
httpBasicAuth: {
|
||||
id: 'yuuJAO2Ang5B64wd',
|
||||
name: 'Unnamed credential',
|
||||
},
|
||||
activeCampaignApi: {
|
||||
id: 'SFCbnfgRBuSzRu6N',
|
||||
name: 'ActiveCampaign account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
pinData: {},
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.httpRequest'],
|
||||
node_connections: [],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'dcc4a9e1-c2c5-4d7e-aec0-2a23adabbb77',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.1,
|
||||
position: [320, 220],
|
||||
credential_type: 'activeCampaignApi',
|
||||
credential_set: true,
|
||||
domain_base: 'google.com',
|
||||
domain_path: '/path/test',
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'HTTP Request V4 with predefined cred': '0' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return graph with http v1 node', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
url: 'https://google.com',
|
||||
options: {},
|
||||
},
|
||||
id: 'b468b603-3e59-4515-b555-90cfebd64d47',
|
||||
name: 'HTTP Request V1',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 1,
|
||||
position: [320, 460],
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
pinData: {},
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.httpRequest'],
|
||||
node_connections: [],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'b468b603-3e59-4515-b555-90cfebd64d47',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 1,
|
||||
position: [320, 460],
|
||||
domain: 'google.com',
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'HTTP Request V1': '0' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should return graph with http v4 node with no parameters and no credentials', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
options: {},
|
||||
},
|
||||
id: 'd002e66f-deba-455c-9f8b-65239db453c3',
|
||||
name: 'HTTP Request v4 with defaults',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.1,
|
||||
position: [600, 240],
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
pinData: {},
|
||||
};
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: ['n8n-nodes-base.httpRequest'],
|
||||
node_connections: [],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'd002e66f-deba-455c-9f8b-65239db453c3',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.1,
|
||||
position: [600, 240],
|
||||
credential_set: false,
|
||||
domain_base: '',
|
||||
domain_path: '',
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: { 'HTTP Request v4 with defaults': '0' },
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should support custom connections like in AI nodes', () => {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'fe69383c-e418-4f98-9c0e-924deafa7f93',
|
||||
name: 'When clicking "Test Workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [540, 220],
|
||||
},
|
||||
{
|
||||
parameters: {},
|
||||
id: 'c5c374f1-6fad-46bb-8eea-ceec126b300a',
|
||||
name: 'Chain',
|
||||
type: '@n8n/n8n-nodes-langchain.chainLlm',
|
||||
typeVersion: 1,
|
||||
position: [760, 320],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
options: {},
|
||||
},
|
||||
id: '198133b6-95dd-4f7e-90e5-e16c4cdbad12',
|
||||
name: 'Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [780, 500],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking "Test Workflow"': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Chain',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Model: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{
|
||||
node: 'Chain',
|
||||
type: 'ai_languageModel',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(generateNodesGraph(workflow, nodeTypes)).toEqual({
|
||||
nodeGraph: {
|
||||
node_types: [
|
||||
'n8n-nodes-base.manualTrigger',
|
||||
'@n8n/n8n-nodes-langchain.chainLlm',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
],
|
||||
node_connections: [
|
||||
{
|
||||
start: '0',
|
||||
end: '1',
|
||||
},
|
||||
{
|
||||
start: '2',
|
||||
end: '1',
|
||||
},
|
||||
],
|
||||
nodes: {
|
||||
'0': {
|
||||
id: 'fe69383c-e418-4f98-9c0e-924deafa7f93',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
position: [540, 220],
|
||||
},
|
||||
'1': {
|
||||
id: 'c5c374f1-6fad-46bb-8eea-ceec126b300a',
|
||||
type: '@n8n/n8n-nodes-langchain.chainLlm',
|
||||
version: 1,
|
||||
position: [760, 320],
|
||||
},
|
||||
'2': {
|
||||
id: '198133b6-95dd-4f7e-90e5-e16c4cdbad12',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
version: 1,
|
||||
position: [780, 500],
|
||||
},
|
||||
},
|
||||
notes: {},
|
||||
is_pinned: false,
|
||||
},
|
||||
nameIndices: {
|
||||
'When clicking "Test Workflow"': '0',
|
||||
Chain: '1',
|
||||
Model: '2',
|
||||
},
|
||||
webhookNodeNames: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function validUrls(idMaker: typeof alphanumericId | typeof email, char = CHAR) {
|
||||
const firstId = idMaker();
|
||||
const secondId = idMaker();
|
||||
|
|
Loading…
Reference in a new issue