From a8dd35b0f020b205f7bcb1ee0bf0a2c6279aab99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 20 Dec 2024 14:40:06 +0100 Subject: [PATCH 01/66] refactor(core): Break up more code in the execution engine, and add tests (no-changelog) (#12320) --- packages/core/src/NodeExecuteFunctions.ts | 191 +------- .../__tests__/node-execution-context.test.ts | 180 +++++++- .../base-execute-context.ts | 22 - .../node-execution-context/execute-context.ts | 2 +- .../core/src/node-execution-context/index.ts | 3 +- .../node-execution-context.ts | 51 ++- .../supply-data-context.ts | 2 +- .../core/src/node-execution-context/utils.ts | 423 ------------------ .../__tests__/cleanupParameterData.test.ts | 38 ++ .../utils/__tests__/ensureType.test.ts | 80 ++++ .../utils/__tests__/getAdditionalKeys.test.ts | 146 ++++++ .../__tests__/getInputConnectionData.test.ts | 366 +++++++++++++++ .../validateValueAgainstSchema.test.ts} | 117 +---- .../utils/cleanupParameterData.ts | 31 ++ .../utils/ensureType.ts | 103 +++++ .../utils/getAdditionalKeys.ts | 74 +++ .../utils/getInputConnectionData.ts | 184 ++++++++ .../utils/validateValueAgainstSchema.ts | 218 +++++++++ .../node-execution-context/webhook-context.ts | 2 +- packages/workflow/src/Interfaces.ts | 23 +- 20 files changed, 1492 insertions(+), 764 deletions(-) delete mode 100644 packages/core/src/node-execution-context/utils.ts create mode 100644 packages/core/src/node-execution-context/utils/__tests__/cleanupParameterData.test.ts create mode 100644 packages/core/src/node-execution-context/utils/__tests__/ensureType.test.ts create mode 100644 packages/core/src/node-execution-context/utils/__tests__/getAdditionalKeys.test.ts create mode 100644 packages/core/src/node-execution-context/utils/__tests__/getInputConnectionData.test.ts rename packages/core/src/node-execution-context/{__tests__/utils.test.ts => utils/__tests__/validateValueAgainstSchema.test.ts} (59%) create mode 100644 packages/core/src/node-execution-context/utils/cleanupParameterData.ts create mode 100644 packages/core/src/node-execution-context/utils/ensureType.ts create mode 100644 packages/core/src/node-execution-context/utils/getAdditionalKeys.ts create mode 100644 packages/core/src/node-execution-context/utils/getInputConnectionData.ts create mode 100644 packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 38679513de..20d5ae0833 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -31,7 +31,6 @@ import pick from 'lodash/pick'; import { extension, lookup } from 'mime-types'; import type { BinaryHelperFunctions, - CloseFunction, FileSystemHelperFunctions, GenericValue, IAdditionalCredentialOptions, @@ -47,7 +46,6 @@ import type { IN8nHttpResponse, INode, INodeExecutionData, - INodeInputConfiguration, IOAuth2Options, IPairedItemData, IPollFunctions, @@ -76,11 +74,8 @@ import type { ICheckProcessedContextData, WebhookType, SchedulingFunctions, - SupplyData, - AINodeConnectionType, } from 'n8n-workflow'; import { - NodeConnectionType, LoggerProxy as Logger, NodeApiError, NodeHelpers, @@ -114,12 +109,11 @@ import { UM_EMAIL_TEMPLATES_INVITE, UM_EMAIL_TEMPLATES_PWRESET, } from './Constants'; -import { createNodeAsTool } from './CreateNodeAsTool'; import { DataDeduplicationService } from './data-deduplication-service'; import { InstanceSettings } from './InstanceSettings'; import type { IResponseError } from './Interfaces'; // eslint-disable-next-line import/no-cycle -import { PollContext, SupplyDataContext, TriggerContext } from './node-execution-context'; +import { PollContext, TriggerContext } from './node-execution-context'; import { ScheduledTaskManager } from './ScheduledTaskManager'; import { SSHClientsManager } from './SSHClientsManager'; @@ -2013,185 +2007,6 @@ export function getWebhookDescription( return undefined; } -export async function getInputConnectionData( - this: IAllExecuteFunctions, - workflow: Workflow, - runExecutionData: IRunExecutionData, - parentRunIndex: number, - connectionInputData: INodeExecutionData[], - parentInputData: ITaskDataConnections, - additionalData: IWorkflowExecuteAdditionalData, - executeData: IExecuteData, - mode: WorkflowExecuteMode, - closeFunctions: CloseFunction[], - connectionType: AINodeConnectionType, - itemIndex: number, - abortSignal?: AbortSignal, -): Promise { - const parentNode = this.getNode(); - const parentNodeType = workflow.nodeTypes.getByNameAndVersion( - parentNode.type, - parentNode.typeVersion, - ); - - const inputs = NodeHelpers.getNodeInputs(workflow, parentNode, parentNodeType.description); - - let inputConfiguration = inputs.find((input) => { - if (typeof input === 'string') { - return input === connectionType; - } - return input.type === connectionType; - }); - - if (inputConfiguration === undefined) { - throw new ApplicationError('Node does not have input of type', { - extra: { nodeName: parentNode.name, connectionType }, - }); - } - - if (typeof inputConfiguration === 'string') { - inputConfiguration = { - type: inputConfiguration, - } as INodeInputConfiguration; - } - - const connectedNodes = workflow - .getParentNodes(parentNode.name, connectionType, 1) - .map((nodeName) => workflow.getNode(nodeName) as INode) - .filter((connectedNode) => connectedNode.disabled !== true); - - if (connectedNodes.length === 0) { - if (inputConfiguration.required) { - throw new NodeOperationError( - parentNode, - `A ${inputConfiguration?.displayName ?? connectionType} sub-node must be connected and enabled`, - ); - } - return inputConfiguration.maxConnections === 1 ? undefined : []; - } - - if ( - inputConfiguration.maxConnections !== undefined && - connectedNodes.length > inputConfiguration.maxConnections - ) { - throw new NodeOperationError( - parentNode, - `Only ${inputConfiguration.maxConnections} ${connectionType} sub-nodes are/is allowed to be connected`, - ); - } - - const nodes: SupplyData[] = []; - for (const connectedNode of connectedNodes) { - const connectedNodeType = workflow.nodeTypes.getByNameAndVersion( - connectedNode.type, - connectedNode.typeVersion, - ); - const contextFactory = (runIndex: number, inputData: ITaskDataConnections) => - new SupplyDataContext( - workflow, - connectedNode, - additionalData, - mode, - runExecutionData, - runIndex, - connectionInputData, - inputData, - connectionType, - executeData, - closeFunctions, - abortSignal, - ); - - if (!connectedNodeType.supplyData) { - if (connectedNodeType.description.outputs.includes(NodeConnectionType.AiTool)) { - /** - * This keeps track of how many times this specific AI tool node has been invoked. - * It is incremented on every invocation of the tool to keep the output of each invocation separate from each other. - */ - let toolRunIndex = 0; - const supplyData = createNodeAsTool({ - node: connectedNode, - nodeType: connectedNodeType, - handleToolInvocation: async (toolArgs) => { - const runIndex = toolRunIndex++; - const context = contextFactory(runIndex, {}); - context.addInputData(NodeConnectionType.AiTool, [[{ json: toolArgs }]]); - - try { - // Execute the sub-node with the proxied context - const result = await connectedNodeType.execute?.call( - context as unknown as IExecuteFunctions, - ); - - // Process and map the results - const mappedResults = result?.[0]?.flatMap((item) => item.json); - - // Add output data to the context - context.addOutputData(NodeConnectionType.AiTool, runIndex, [ - [{ json: { response: mappedResults } }], - ]); - - // Return the stringified results - return JSON.stringify(mappedResults); - } catch (error) { - const nodeError = new NodeOperationError(connectedNode, error as Error); - context.addOutputData(NodeConnectionType.AiTool, runIndex, nodeError); - return 'Error during node execution: ' + nodeError.description; - } - }, - }); - nodes.push(supplyData); - } else { - throw new ApplicationError('Node does not have a `supplyData` method defined', { - extra: { nodeName: connectedNode.name }, - }); - } - } else { - const context = contextFactory(parentRunIndex, parentInputData); - try { - const supplyData = await connectedNodeType.supplyData.call(context, itemIndex); - if (supplyData.closeFunction) { - closeFunctions.push(supplyData.closeFunction); - } - nodes.push(supplyData); - } catch (error) { - // Propagate errors from sub-nodes - if (error.functionality === 'configuration-node') throw error; - if (!(error instanceof ExecutionBaseError)) { - error = new NodeOperationError(connectedNode, error, { - itemIndex, - }); - } - - let currentNodeRunIndex = 0; - if (runExecutionData.resultData.runData.hasOwnProperty(parentNode.name)) { - currentNodeRunIndex = runExecutionData.resultData.runData[parentNode.name].length; - } - - // Display the error on the node which is causing it - await context.addExecutionDataFunctions( - 'input', - error, - connectionType, - parentNode.name, - currentNodeRunIndex, - ); - - // Display on the calling node which node has the error - throw new NodeOperationError(connectedNode, `Error in sub-node ${connectedNode.name}`, { - itemIndex, - functionality: 'configuration-node', - description: error.message, - }); - } - } - } - - return inputConfiguration.maxConnections === 1 - ? (nodes || [])[0]?.response - : nodes.map((node) => node.response); -} - export const getRequestHelperFunctions = ( workflow: Workflow, node: INode, @@ -2254,7 +2069,7 @@ export const getRequestHelperFunctions = ( const runIndex = 0; - const additionalKeys = { + const additionalKeys: IWorkflowDataProxyAdditionalKeys = { $request: requestOptions, $response: {} as IN8nHttpFullResponse, $version: node.typeVersion, @@ -2379,7 +2194,7 @@ export const getRequestHelperFunctions = ( responseData.push(tempResponseData); additionalKeys.$response = newResponse; - additionalKeys.$pageCount = additionalKeys.$pageCount + 1; + additionalKeys.$pageCount = (additionalKeys.$pageCount ?? 0) + 1; const maxRequests = getResolvedValue( paginationOptions.maxRequests, diff --git a/packages/core/src/node-execution-context/__tests__/node-execution-context.test.ts b/packages/core/src/node-execution-context/__tests__/node-execution-context.test.ts index 0231873984..443460bb33 100644 --- a/packages/core/src/node-execution-context/__tests__/node-execution-context.test.ts +++ b/packages/core/src/node-execution-context/__tests__/node-execution-context.test.ts @@ -1,11 +1,15 @@ import { mock } from 'jest-mock-extended'; import type { + Expression, INode, + INodeType, + INodeTypes, INodeExecutionData, IWorkflowExecuteAdditionalData, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; import { Container } from 'typedi'; import { InstanceSettings } from '@/InstanceSettings'; @@ -18,23 +22,29 @@ describe('NodeExecutionContext', () => { const instanceSettings = mock({ instanceId: 'abc123' }); Container.set(InstanceSettings, instanceSettings); + const node = mock(); + const nodeType = mock({ description: mock() }); + const nodeTypes = mock(); + const expression = mock(); const workflow = mock({ id: '123', name: 'Test Workflow', active: true, - nodeTypes: mock(), + nodeTypes, timezone: 'UTC', + expression, }); - const node = mock(); let additionalData = mock({ credentialsHelper: mock(), }); const mode: WorkflowExecuteMode = 'manual'; - const testContext = new TestContext(workflow, node, additionalData, mode); + let testContext: TestContext; beforeEach(() => { jest.clearAllMocks(); + testContext = new TestContext(workflow, node, additionalData, mode); + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); }); describe('getNode', () => { @@ -106,9 +116,9 @@ describe('NodeExecutionContext', () => { }); describe('getKnownNodeTypes', () => { - it('should call getKnownTypes method of workflow.nodeTypes', () => { + it('should call getKnownTypes method of nodeTypes', () => { testContext.getKnownNodeTypes(); - expect(workflow.nodeTypes.getKnownTypes).toHaveBeenCalled(); + expect(nodeTypes.getKnownTypes).toHaveBeenCalled(); }); }); @@ -165,4 +175,164 @@ describe('NodeExecutionContext', () => { expect(result).toEqual([outputData]); }); }); + + describe('getNodeInputs', () => { + it('should return static inputs array when inputs is an array', () => { + nodeType.description.inputs = [NodeConnectionType.Main, NodeConnectionType.AiLanguageModel]; + + const result = testContext.getNodeInputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel }, + ]); + }); + + it('should return input objects when inputs contains configurations', () => { + nodeType.description.inputs = [ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel, required: true }, + ]; + + const result = testContext.getNodeInputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel, required: true }, + ]); + }); + + it('should evaluate dynamic inputs when inputs is a function', () => { + const inputsExpressions = '={{ ["main", "ai_languageModel"] }}'; + nodeType.description.inputs = inputsExpressions; + expression.getSimpleParameterValue.mockReturnValue([ + NodeConnectionType.Main, + NodeConnectionType.AiLanguageModel, + ]); + + const result = testContext.getNodeInputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel }, + ]); + expect(expression.getSimpleParameterValue).toHaveBeenCalledWith( + node, + inputsExpressions, + 'internal', + {}, + ); + }); + }); + + describe('getNodeOutputs', () => { + it('should return static outputs array when outputs is an array', () => { + nodeType.description.outputs = [NodeConnectionType.Main, NodeConnectionType.AiLanguageModel]; + + const result = testContext.getNodeOutputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel }, + ]); + }); + + it('should return output objects when outputs contains configurations', () => { + nodeType.description.outputs = [ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel, required: true }, + ]; + + const result = testContext.getNodeOutputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel, required: true }, + ]); + }); + + it('should evaluate dynamic outputs when outputs is a function', () => { + const outputsExpressions = '={{ ["main", "ai_languageModel"] }}'; + nodeType.description.outputs = outputsExpressions; + expression.getSimpleParameterValue.mockReturnValue([ + NodeConnectionType.Main, + NodeConnectionType.AiLanguageModel, + ]); + + const result = testContext.getNodeOutputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main }, + { type: NodeConnectionType.AiLanguageModel }, + ]); + expect(expression.getSimpleParameterValue).toHaveBeenCalledWith( + node, + outputsExpressions, + 'internal', + {}, + ); + }); + + it('should add error output when node has continueOnFail error handling', () => { + const nodeWithError = mock({ onError: 'continueErrorOutput' }); + const contextWithError = new TestContext(workflow, nodeWithError, additionalData, mode); + nodeType.description.outputs = [NodeConnectionType.Main]; + + const result = contextWithError.getNodeOutputs(); + + expect(result).toEqual([ + { type: NodeConnectionType.Main, displayName: 'Success' }, + { type: NodeConnectionType.Main, displayName: 'Error', category: 'error' }, + ]); + }); + }); + + describe('getConnectedNodes', () => { + it('should return connected nodes of given type', () => { + const node1 = mock({ name: 'Node 1', type: 'test', disabled: false }); + const node2 = mock({ name: 'Node 2', type: 'test', disabled: false }); + + workflow.getParentNodes.mockReturnValue(['Node 1', 'Node 2']); + workflow.getNode.mockImplementation((name) => { + if (name === 'Node 1') return node1; + if (name === 'Node 2') return node2; + return null; + }); + + const result = testContext.getConnectedNodes(NodeConnectionType.Main); + + expect(result).toEqual([node1, node2]); + expect(workflow.getParentNodes).toHaveBeenCalledWith(node.name, NodeConnectionType.Main, 1); + }); + + it('should filter out disabled nodes', () => { + const node1 = mock({ name: 'Node 1', type: 'test', disabled: false }); + const node2 = mock({ name: 'Node 2', type: 'test', disabled: true }); + + workflow.getParentNodes.mockReturnValue(['Node 1', 'Node 2']); + workflow.getNode.mockImplementation((name) => { + if (name === 'Node 1') return node1; + if (name === 'Node 2') return node2; + return null; + }); + + const result = testContext.getConnectedNodes(NodeConnectionType.Main); + + expect(result).toEqual([node1]); + }); + + it('should filter out non-existent nodes', () => { + const node1 = mock({ name: 'Node 1', type: 'test', disabled: false }); + + workflow.getParentNodes.mockReturnValue(['Node 1', 'NonExistent']); + workflow.getNode.mockImplementation((name) => { + if (name === 'Node 1') return node1; + return null; + }); + + const result = testContext.getConnectedNodes(NodeConnectionType.Main); + + expect(result).toEqual([node1]); + }); + }); }); diff --git a/packages/core/src/node-execution-context/base-execute-context.ts b/packages/core/src/node-execution-context/base-execute-context.ts index 8ecc658579..24b9e89301 100644 --- a/packages/core/src/node-execution-context/base-execute-context.ts +++ b/packages/core/src/node-execution-context/base-execute-context.ts @@ -16,8 +16,6 @@ import type { ITaskMetadata, ContextType, IContextObject, - INodeInputConfiguration, - INodeOutputConfiguration, IWorkflowDataProxyData, ISourceData, AiEvent, @@ -161,26 +159,6 @@ export class BaseExecuteContext extends NodeExecutionContext { return allItems; } - getNodeInputs(): INodeInputConfiguration[] { - const nodeType = this.workflow.nodeTypes.getByNameAndVersion( - this.node.type, - this.node.typeVersion, - ); - return NodeHelpers.getNodeInputs(this.workflow, this.node, nodeType.description).map((input) => - typeof input === 'string' ? { type: input } : input, - ); - } - - getNodeOutputs(): INodeOutputConfiguration[] { - const nodeType = this.workflow.nodeTypes.getByNameAndVersion( - this.node.type, - this.node.typeVersion, - ); - return NodeHelpers.getNodeOutputs(this.workflow, this.node, nodeType.description).map( - (output) => (typeof output === 'string' ? { type: output } : output), - ); - } - getInputSourceData(inputIndex = 0, connectionType = NodeConnectionType.Main): ISourceData { if (this.executeData?.source === null) { // Should never happen as n8n sets it automatically diff --git a/packages/core/src/node-execution-context/execute-context.ts b/packages/core/src/node-execution-context/execute-context.ts index d563881bea..f3e32608f3 100644 --- a/packages/core/src/node-execution-context/execute-context.ts +++ b/packages/core/src/node-execution-context/execute-context.ts @@ -28,7 +28,6 @@ import { copyInputItems, normalizeItems, constructExecutionMetaData, - getInputConnectionData, assertBinaryData, getBinaryDataBuffer, copyBinaryFile, @@ -41,6 +40,7 @@ import { } from '@/NodeExecuteFunctions'; import { BaseExecuteContext } from './base-execute-context'; +import { getInputConnectionData } from './utils/getInputConnectionData'; export class ExecuteContext extends BaseExecuteContext implements IExecuteFunctions { readonly helpers: IExecuteFunctions['helpers']; diff --git a/packages/core/src/node-execution-context/index.ts b/packages/core/src/node-execution-context/index.ts index 00c90266db..64088af72e 100644 --- a/packages/core/src/node-execution-context/index.ts +++ b/packages/core/src/node-execution-context/index.ts @@ -4,8 +4,9 @@ export { ExecuteSingleContext } from './execute-single-context'; export { HookContext } from './hook-context'; export { LoadOptionsContext } from './load-options-context'; export { PollContext } from './poll-context'; +// eslint-disable-next-line import/no-cycle export { SupplyDataContext } from './supply-data-context'; export { TriggerContext } from './trigger-context'; export { WebhookContext } from './webhook-context'; -export { getAdditionalKeys } from './utils'; +export { getAdditionalKeys } from './utils/getAdditionalKeys'; diff --git a/packages/core/src/node-execution-context/node-execution-context.ts b/packages/core/src/node-execution-context/node-execution-context.ts index 158b06d02e..8477fe7856 100644 --- a/packages/core/src/node-execution-context/node-execution-context.ts +++ b/packages/core/src/node-execution-context/node-execution-context.ts @@ -9,8 +9,11 @@ import type { INodeCredentialDescription, INodeCredentialsDetails, INodeExecutionData, + INodeInputConfiguration, + INodeOutputConfiguration, IRunExecutionData, IWorkflowExecuteAdditionalData, + NodeConnectionType, NodeParameterValueType, NodeTypeAndVersion, Workflow, @@ -27,15 +30,14 @@ import { import { Container } from 'typedi'; import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/Constants'; +import { Memoized } from '@/decorators'; import { extractValue } from '@/ExtractValue'; import { InstanceSettings } from '@/InstanceSettings'; -import { - cleanupParameterData, - ensureType, - getAdditionalKeys, - validateValueAgainstSchema, -} from './utils'; +import { cleanupParameterData } from './utils/cleanupParameterData'; +import { ensureType } from './utils/ensureType'; +import { getAdditionalKeys } from './utils/getAdditionalKeys'; +import { validateValueAgainstSchema } from './utils/validateValueAgainstSchema'; export abstract class NodeExecutionContext implements Omit { protected readonly instanceSettings = Container.get(InstanceSettings); @@ -108,6 +110,42 @@ export abstract class NodeExecutionContext implements Omit (typeof input === 'string' ? { type: input } : input), + ); + } + + getNodeInputs(): INodeInputConfiguration[] { + return this.nodeInputs; + } + + @Memoized + get nodeOutputs() { + return NodeHelpers.getNodeOutputs(this.workflow, this.node, this.nodeType.description).map( + (output) => (typeof output === 'string' ? { type: output } : output), + ); + } + + getConnectedNodes(connectionType: NodeConnectionType): INode[] { + return this.workflow + .getParentNodes(this.node.name, connectionType, 1) + .map((nodeName) => this.workflow.getNode(nodeName)) + .filter((node) => !!node) + .filter((node) => node.disabled !== true); + } + + getNodeOutputs(): INodeOutputConfiguration[] { + return this.nodeOutputs; + } + getKnownNodeTypes() { return this.workflow.nodeTypes.getKnownTypes(); } @@ -260,6 +298,7 @@ export abstract class NodeExecutionContext implements Omit cleanupParameterData(value as NodeParameterValueType)); - return; - } - - if (typeof inputData === 'object') { - Object.keys(inputData).forEach((key) => { - const value = (inputData as INodeParameters)[key]; - if (typeof value === 'object') { - if (DateTime.isDateTime(value)) { - // Is a special luxon date so convert to string - (inputData as INodeParameters)[key] = value.toString(); - } else { - cleanupParameterData(value); - } - } - }); - } -} - -const validateResourceMapperValue = ( - parameterName: string, - paramValues: { [key: string]: unknown }, - node: INode, - skipRequiredCheck = false, -): ExtendedValidationResult => { - const result: ExtendedValidationResult = { valid: true, newValue: paramValues }; - const paramNameParts = parameterName.split('.'); - if (paramNameParts.length !== 2) { - return result; - } - const resourceMapperParamName = paramNameParts[0]; - const resourceMapperField = node.parameters[resourceMapperParamName]; - if (!resourceMapperField || !isResourceMapperValue(resourceMapperField)) { - return result; - } - const schema = resourceMapperField.schema; - const paramValueNames = Object.keys(paramValues); - for (let i = 0; i < paramValueNames.length; i++) { - const key = paramValueNames[i]; - const resolvedValue = paramValues[key]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call - const schemaEntry = schema.find((s) => s.id === key); - - if ( - !skipRequiredCheck && - schemaEntry?.required === true && - schemaEntry.type !== 'boolean' && - !resolvedValue - ) { - return { - valid: false, - errorMessage: `The value "${String(key)}" is required but not set`, - fieldName: key, - }; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (schemaEntry?.type) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - valueOptions: schemaEntry.options, - }); - if (!validationResult.valid) { - return { ...validationResult, fieldName: key }; - } else { - // If it's valid, set the casted value - paramValues[key] = validationResult.newValue; - } - } - } - return result; -}; - -const validateCollection = ( - node: INode, - runIndex: number, - itemIndex: number, - propertyDescription: INodeProperties, - parameterPath: string[], - validationResult: ExtendedValidationResult, -): ExtendedValidationResult => { - let nestedDescriptions: INodeProperties[] | undefined; - - if (propertyDescription.type === 'fixedCollection') { - nestedDescriptions = (propertyDescription.options as INodePropertyCollection[]).find( - (entry) => entry.name === parameterPath[1], - )?.values; - } - - if (propertyDescription.type === 'collection') { - nestedDescriptions = propertyDescription.options as INodeProperties[]; - } - - if (!nestedDescriptions) { - return validationResult; - } - - const validationMap: { - [key: string]: { type: FieldType; displayName: string; options?: INodePropertyOptions[] }; - } = {}; - - for (const prop of nestedDescriptions) { - if (!prop.validateType || prop.ignoreValidationDuringExecution) continue; - - validationMap[prop.name] = { - type: prop.validateType, - displayName: prop.displayName, - options: - prop.validateType === 'options' ? (prop.options as INodePropertyOptions[]) : undefined, - }; - } - - if (!Object.keys(validationMap).length) { - return validationResult; - } - - if (validationResult.valid) { - for (const value of Array.isArray(validationResult.newValue) - ? (validationResult.newValue as IDataObject[]) - : [validationResult.newValue as IDataObject]) { - for (const key of Object.keys(value)) { - if (!validationMap[key]) continue; - - const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, { - valueOptions: validationMap[key].options, - }); - - if (!fieldValidationResult.valid) { - throw new ExpressionError( - `Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`, - { - description: fieldValidationResult.errorMessage, - runIndex, - itemIndex, - nodeCause: node.name, - }, - ); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - value[key] = fieldValidationResult.newValue; - } - } - } - - return validationResult; -}; - -export const validateValueAgainstSchema = ( - node: INode, - nodeType: INodeType, - parameterValue: string | number | boolean | object | null | undefined, - parameterName: string, - runIndex: number, - itemIndex: number, -) => { - const parameterPath = parameterName.split('.'); - - const propertyDescription = nodeType.description.properties.find( - (prop) => - parameterPath[0] === prop.name && NodeHelpers.displayParameter(node.parameters, prop, node), - ); - - if (!propertyDescription) { - return parameterValue; - } - - let validationResult: ExtendedValidationResult = { valid: true, newValue: parameterValue }; - - if ( - parameterPath.length === 1 && - propertyDescription.validateType && - !propertyDescription.ignoreValidationDuringExecution - ) { - validationResult = validateFieldType( - parameterName, - parameterValue, - propertyDescription.validateType, - ); - } else if ( - propertyDescription.type === 'resourceMapper' && - parameterPath[1] === 'value' && - typeof parameterValue === 'object' - ) { - validationResult = validateResourceMapperValue( - parameterName, - parameterValue as { [key: string]: unknown }, - node, - propertyDescription.typeOptions?.resourceMapper?.mode !== 'add', - ); - } else if (['fixedCollection', 'collection'].includes(propertyDescription.type)) { - validationResult = validateCollection( - node, - runIndex, - itemIndex, - propertyDescription, - parameterPath, - validationResult, - ); - } - - if (!validationResult.valid) { - throw new ExpressionError( - `Invalid input for '${ - validationResult.fieldName - ? String(validationResult.fieldName) - : propertyDescription.displayName - }' [item ${itemIndex}]`, - { - description: validationResult.errorMessage, - runIndex, - itemIndex, - nodeCause: node.name, - }, - ); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return validationResult.newValue; -}; - -export function ensureType( - toType: EnsureTypeOptions, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameterValue: any, - parameterName: string, - errorOptions?: { itemIndex?: number; runIndex?: number; nodeCause?: string }, -): string | number | boolean | object { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - let returnData = parameterValue; - - if (returnData === null) { - throw new ExpressionError(`Parameter '${parameterName}' must not be null`, errorOptions); - } - - if (returnData === undefined) { - throw new ExpressionError( - `Parameter '${parameterName}' could not be 'undefined'`, - errorOptions, - ); - } - - if (['object', 'array', 'json'].includes(toType)) { - if (typeof returnData !== 'object') { - // if value is not an object and is string try to parse it, else throw an error - if (typeof returnData === 'string' && returnData.length) { - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsedValue = JSON.parse(returnData); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - returnData = parsedValue; - } catch (error) { - throw new ExpressionError(`Parameter '${parameterName}' could not be parsed`, { - ...errorOptions, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - description: error.message, - }); - } - } else { - throw new ExpressionError( - `Parameter '${parameterName}' must be an ${toType}, but we got '${String(parameterValue)}'`, - errorOptions, - ); - } - } else if (toType === 'json') { - // value is an object, make sure it is valid JSON - try { - JSON.stringify(returnData); - } catch (error) { - throw new ExpressionError(`Parameter '${parameterName}' is not valid JSON`, { - ...errorOptions, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - description: error.message, - }); - } - } - - if (toType === 'array' && !Array.isArray(returnData)) { - // value is not an array, but has to be - throw new ExpressionError( - `Parameter '${parameterName}' must be an array, but we got object`, - errorOptions, - ); - } - } - - try { - if (toType === 'string') { - if (typeof returnData === 'object') { - returnData = JSON.stringify(returnData); - } else { - returnData = String(returnData); - } - } - - if (toType === 'number') { - returnData = Number(returnData); - if (Number.isNaN(returnData)) { - throw new ExpressionError( - `Parameter '${parameterName}' must be a number, but we got '${parameterValue}'`, - errorOptions, - ); - } - } - - if (toType === 'boolean') { - returnData = Boolean(returnData); - } - } catch (error) { - if (error instanceof ExpressionError) throw error; - - throw new ExpressionError(`Parameter '${parameterName}' could not be converted to ${toType}`, { - ...errorOptions, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - description: error.message, - }); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return returnData; -} - -/** Returns the additional keys for Expressions and Function-Nodes */ -export function getAdditionalKeys( - additionalData: IWorkflowExecuteAdditionalData, - mode: WorkflowExecuteMode, - runExecutionData: IRunExecutionData | null, - options?: { secretsEnabled?: boolean }, -): IWorkflowDataProxyAdditionalKeys { - const executionId = additionalData.executionId ?? PLACEHOLDER_EMPTY_EXECUTION_ID; - const resumeUrl = `${additionalData.webhookWaitingBaseUrl}/${executionId}`; - const resumeFormUrl = `${additionalData.formWaitingBaseUrl}/${executionId}`; - return { - $execution: { - id: executionId, - mode: mode === 'manual' ? 'test' : 'production', - resumeUrl, - resumeFormUrl, - customData: runExecutionData - ? { - set(key: string, value: string): void { - try { - setWorkflowExecutionMetadata(runExecutionData, key, value); - } catch (e) { - if (mode === 'manual') { - throw e; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access - LoggerProxy.debug(e.message); - } - }, - setAll(obj: Record): void { - try { - setAllWorkflowExecutionMetadata(runExecutionData, obj); - } catch (e) { - if (mode === 'manual') { - throw e; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access - LoggerProxy.debug(e.message); - } - }, - get(key: string): string { - return getWorkflowExecutionMetadata(runExecutionData, key); - }, - getAll(): Record { - return getAllWorkflowExecutionMetadata(runExecutionData); - }, - } - : undefined, - }, - $vars: additionalData.variables, - $secrets: options?.secretsEnabled ? getSecretsProxy(additionalData) : undefined, - - // deprecated - $executionId: executionId, - $resumeWebhookUrl: resumeUrl, - }; -} diff --git a/packages/core/src/node-execution-context/utils/__tests__/cleanupParameterData.test.ts b/packages/core/src/node-execution-context/utils/__tests__/cleanupParameterData.test.ts new file mode 100644 index 0000000000..47913669b6 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/__tests__/cleanupParameterData.test.ts @@ -0,0 +1,38 @@ +import toPlainObject from 'lodash/toPlainObject'; +import { DateTime } from 'luxon'; +import type { NodeParameterValue } from 'n8n-workflow'; + +import { cleanupParameterData } from '../cleanupParameterData'; + +describe('cleanupParameterData', () => { + it('should stringify Luxon dates in-place', () => { + const input = { x: 1, y: DateTime.now() as unknown as NodeParameterValue }; + expect(typeof input.y).toBe('object'); + cleanupParameterData(input); + expect(typeof input.y).toBe('string'); + }); + + it('should stringify plain Luxon dates in-place', () => { + const input = { + x: 1, + y: toPlainObject(DateTime.now()), + }; + expect(typeof input.y).toBe('object'); + cleanupParameterData(input); + expect(typeof input.y).toBe('string'); + }); + + it('should handle objects with nameless constructors', () => { + const input = { x: 1, y: { constructor: {} } as NodeParameterValue }; + expect(typeof input.y).toBe('object'); + cleanupParameterData(input); + expect(typeof input.y).toBe('object'); + }); + + it('should handle objects without a constructor', () => { + const input = { x: 1, y: { constructor: undefined } as unknown as NodeParameterValue }; + expect(typeof input.y).toBe('object'); + cleanupParameterData(input); + expect(typeof input.y).toBe('object'); + }); +}); diff --git a/packages/core/src/node-execution-context/utils/__tests__/ensureType.test.ts b/packages/core/src/node-execution-context/utils/__tests__/ensureType.test.ts new file mode 100644 index 0000000000..1637d988c9 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/__tests__/ensureType.test.ts @@ -0,0 +1,80 @@ +import { ExpressionError } from 'n8n-workflow'; + +import { ensureType } from '../ensureType'; + +describe('ensureType', () => { + it('throws error for null value', () => { + expect(() => ensureType('string', null, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' must not be null"), + ); + }); + + it('throws error for undefined value', () => { + expect(() => ensureType('string', undefined, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' could not be 'undefined'"), + ); + }); + + it('returns string value without modification', () => { + const value = 'hello'; + const expectedValue = value; + const result = ensureType('string', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('returns number value without modification', () => { + const value = 42; + const expectedValue = value; + const result = ensureType('number', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('returns boolean value without modification', () => { + const value = true; + const expectedValue = value; + const result = ensureType('boolean', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('converts object to string if toType is string', () => { + const value = { name: 'John' }; + const expectedValue = JSON.stringify(value); + const result = ensureType('string', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('converts string to number if toType is number', () => { + const value = '10'; + const expectedValue = 10; + const result = ensureType('number', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('throws error for invalid conversion to number', () => { + const value = 'invalid'; + expect(() => ensureType('number', value, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"), + ); + }); + + it('parses valid JSON string to object if toType is object', () => { + const value = '{"name": "Alice"}'; + const expectedValue = JSON.parse(value); + const result = ensureType('object', value, 'myParam'); + expect(result).toEqual(expectedValue); + }); + + it('throws error for invalid JSON string to object conversion', () => { + const value = 'invalid_json'; + expect(() => ensureType('object', value, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' could not be parsed"), + ); + }); + + it('throws error for non-array value if toType is array', () => { + const value = { name: 'Alice' }; + expect(() => ensureType('array', value, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' must be an array, but we got object"), + ); + }); +}); diff --git a/packages/core/src/node-execution-context/utils/__tests__/getAdditionalKeys.test.ts b/packages/core/src/node-execution-context/utils/__tests__/getAdditionalKeys.test.ts new file mode 100644 index 0000000000..6ac1fbdc07 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/__tests__/getAdditionalKeys.test.ts @@ -0,0 +1,146 @@ +import { mock } from 'jest-mock-extended'; +import { LoggerProxy } from 'n8n-workflow'; +import type { + IDataObject, + IRunExecutionData, + IWorkflowExecuteAdditionalData, + SecretsHelpersBase, +} from 'n8n-workflow'; + +import { PLACEHOLDER_EMPTY_EXECUTION_ID } from '@/Constants'; + +import { getAdditionalKeys } from '../getAdditionalKeys'; + +describe('getAdditionalKeys', () => { + const secretsHelpers = mock(); + const additionalData = mock({ + executionId: '123', + webhookWaitingBaseUrl: 'https://webhook.test', + formWaitingBaseUrl: 'https://form.test', + variables: { testVar: 'value' }, + secretsHelpers, + }); + + const runExecutionData = mock({ + resultData: { + runData: {}, + metadata: {}, + }, + }); + + beforeAll(() => { + LoggerProxy.init(mock()); + secretsHelpers.hasProvider.mockReturnValue(true); + secretsHelpers.hasSecret.mockReturnValue(true); + secretsHelpers.getSecret.mockReturnValue('secret-value'); + secretsHelpers.listSecrets.mockReturnValue(['secret1']); + secretsHelpers.listProviders.mockReturnValue(['provider1']); + }); + + it('should use placeholder execution ID when none provided', () => { + const noIdData = { ...additionalData, executionId: undefined }; + const result = getAdditionalKeys(noIdData, 'manual', null); + + expect(result.$execution?.id).toBe(PLACEHOLDER_EMPTY_EXECUTION_ID); + }); + + it('should return production mode when not manual', () => { + const result = getAdditionalKeys(additionalData, 'internal', null); + + expect(result.$execution?.mode).toBe('production'); + }); + + it('should include customData methods when runExecutionData is provided', () => { + const result = getAdditionalKeys(additionalData, 'manual', runExecutionData); + + expect(result.$execution?.customData).toBeDefined(); + expect(typeof result.$execution?.customData?.set).toBe('function'); + expect(typeof result.$execution?.customData?.setAll).toBe('function'); + expect(typeof result.$execution?.customData?.get).toBe('function'); + expect(typeof result.$execution?.customData?.getAll).toBe('function'); + }); + + it('should handle customData operations correctly', () => { + const result = getAdditionalKeys(additionalData, 'manual', runExecutionData); + const customData = result.$execution?.customData; + + customData?.set('testKey', 'testValue'); + expect(customData?.get('testKey')).toBe('testValue'); + + customData?.setAll({ key1: 'value1', key2: 'value2' }); + const allData = customData?.getAll(); + expect(allData).toEqual({ + testKey: 'testValue', + key1: 'value1', + key2: 'value2', + }); + }); + + it('should include secrets when enabled', () => { + const result = getAdditionalKeys(additionalData, 'manual', null, { secretsEnabled: true }); + + expect(result.$secrets).toBeDefined(); + expect((result.$secrets?.provider1 as IDataObject).secret1).toEqual('secret-value'); + }); + + it('should not include secrets when disabled', () => { + const result = getAdditionalKeys(additionalData, 'manual', null, { secretsEnabled: false }); + + expect(result.$secrets).toBeUndefined(); + }); + + it('should throw errors in manual mode', () => { + const result = getAdditionalKeys(additionalData, 'manual', runExecutionData); + + expect(() => { + result.$execution?.customData?.set('invalid*key', 'value'); + }).toThrow(); + }); + + it('should correctly set resume URLs', () => { + const result = getAdditionalKeys(additionalData, 'manual', null); + + expect(result.$execution?.resumeUrl).toBe('https://webhook.test/123'); + expect(result.$execution?.resumeFormUrl).toBe('https://form.test/123'); + expect(result.$resumeWebhookUrl).toBe('https://webhook.test/123'); // Test deprecated property + }); + + it('should return test mode when manual', () => { + const result = getAdditionalKeys(additionalData, 'manual', null); + + expect(result.$execution?.mode).toBe('test'); + }); + + it('should return variables from additionalData', () => { + const result = getAdditionalKeys(additionalData, 'manual', null); + expect(result.$vars?.testVar).toEqual('value'); + }); + + it('should handle errors in non-manual mode without throwing', () => { + const result = getAdditionalKeys(additionalData, 'internal', runExecutionData); + const customData = result.$execution?.customData; + + expect(() => { + customData?.set('invalid*key', 'value'); + }).not.toThrow(); + }); + + it('should return undefined customData when runExecutionData is null', () => { + const result = getAdditionalKeys(additionalData, 'manual', null); + + expect(result.$execution?.customData).toBeUndefined(); + }); + + it('should respect metadata KV limit', () => { + const result = getAdditionalKeys(additionalData, 'manual', runExecutionData); + const customData = result.$execution?.customData; + + // Add 11 key-value pairs (exceeding the limit of 10) + for (let i = 0; i < 11; i++) { + customData?.set(`key${i}`, `value${i}`); + } + + const allData = customData?.getAll() ?? {}; + expect(Object.keys(allData)).toHaveLength(10); + }); +}); diff --git a/packages/core/src/node-execution-context/utils/__tests__/getInputConnectionData.test.ts b/packages/core/src/node-execution-context/utils/__tests__/getInputConnectionData.test.ts new file mode 100644 index 0000000000..4e634a196e --- /dev/null +++ b/packages/core/src/node-execution-context/utils/__tests__/getInputConnectionData.test.ts @@ -0,0 +1,366 @@ +import type { Tool } from '@langchain/core/tools'; +import { mock } from 'jest-mock-extended'; +import type { + INode, + ITaskDataConnections, + IRunExecutionData, + INodeExecutionData, + IExecuteData, + IWorkflowExecuteAdditionalData, + Workflow, + INodeType, + INodeTypes, +} from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; + +import { ExecuteContext } from '../../execute-context'; + +describe('getInputConnectionData', () => { + const agentNode = mock({ + name: 'Test Agent', + type: 'test.agent', + parameters: {}, + }); + const agentNodeType = mock({ + description: { + inputs: [], + }, + }); + const nodeTypes = mock(); + const workflow = mock({ + id: 'test-workflow', + active: false, + nodeTypes, + }); + const runExecutionData = mock({ + resultData: { runData: {} }, + }); + const connectionInputData = [] as INodeExecutionData[]; + const inputData = {} as ITaskDataConnections; + const executeData = {} as IExecuteData; + + const hooks = mock>(); + const additionalData = mock({ hooks }); + + let executeContext: ExecuteContext; + + beforeEach(() => { + jest.clearAllMocks(); + + executeContext = new ExecuteContext( + workflow, + agentNode, + additionalData, + 'internal', + runExecutionData, + 0, + connectionInputData, + inputData, + executeData, + [], + ); + + jest.spyOn(executeContext, 'getNode').mockReturnValue(agentNode); + nodeTypes.getByNameAndVersion + .calledWith(agentNode.type, expect.anything()) + .mockReturnValue(agentNodeType); + }); + + describe.each([ + NodeConnectionType.AiAgent, + NodeConnectionType.AiChain, + NodeConnectionType.AiDocument, + NodeConnectionType.AiEmbedding, + NodeConnectionType.AiLanguageModel, + NodeConnectionType.AiMemory, + NodeConnectionType.AiOutputParser, + NodeConnectionType.AiRetriever, + NodeConnectionType.AiTextSplitter, + NodeConnectionType.AiVectorStore, + ] as const)('%s', (connectionType) => { + const response = mock(); + const node = mock({ + name: 'First Node', + type: 'test.type', + disabled: false, + }); + const secondNode = mock({ name: 'Second Node', disabled: false }); + const supplyData = jest.fn().mockResolvedValue({ response }); + const nodeType = mock({ supplyData }); + + beforeEach(() => { + nodeTypes.getByNameAndVersion + .calledWith(node.type, expect.anything()) + .mockReturnValue(nodeType); + workflow.getParentNodes + .calledWith(agentNode.name, connectionType) + .mockReturnValue([node.name]); + workflow.getNode.calledWith(node.name).mockReturnValue(node); + workflow.getNode.calledWith(secondNode.name).mockReturnValue(secondNode); + }); + + it('should throw when no inputs are defined', async () => { + agentNodeType.description.inputs = []; + + await expect(executeContext.getInputConnectionData(connectionType, 0)).rejects.toThrow( + 'Node does not have input of type', + ); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should return undefined when no nodes are connected and input is not required', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + maxConnections: 1, + required: false, + }, + ]; + workflow.getParentNodes.mockReturnValueOnce([]); + + const result = await executeContext.getInputConnectionData(connectionType, 0); + expect(result).toBeUndefined(); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should throw when too many nodes are connected', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + maxConnections: 1, + required: true, + }, + ]; + workflow.getParentNodes.mockReturnValueOnce([node.name, secondNode.name]); + + await expect(executeContext.getInputConnectionData(connectionType, 0)).rejects.toThrow( + `Only 1 ${connectionType} sub-nodes are/is allowed to be connected`, + ); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should throw when required node is not connected', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + required: true, + }, + ]; + workflow.getParentNodes.mockReturnValueOnce([]); + + await expect(executeContext.getInputConnectionData(connectionType, 0)).rejects.toThrow( + 'must be connected and enabled', + ); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should handle disabled nodes', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + required: true, + }, + ]; + + const disabledNode = mock({ + name: 'Disabled Node', + type: 'test.type', + disabled: true, + }); + workflow.getParentNodes.mockReturnValueOnce([disabledNode.name]); + workflow.getNode.calledWith(disabledNode.name).mockReturnValue(disabledNode); + + await expect(executeContext.getInputConnectionData(connectionType, 0)).rejects.toThrow( + 'must be connected and enabled', + ); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should handle node execution errors', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + required: true, + }, + ]; + + supplyData.mockRejectedValueOnce(new Error('supplyData error')); + + await expect(executeContext.getInputConnectionData(connectionType, 0)).rejects.toThrow( + `Error in sub-node ${node.name}`, + ); + expect(supplyData).toHaveBeenCalled(); + }); + + it('should propagate configuration errors', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + required: true, + }, + ]; + + const configError = new NodeOperationError(node, 'Config Error in node', { + functionality: 'configuration-node', + }); + supplyData.mockRejectedValueOnce(configError); + + await expect(executeContext.getInputConnectionData(connectionType, 0)).rejects.toThrow( + configError.message, + ); + expect(nodeType.supplyData).toHaveBeenCalled(); + }); + + it('should handle close functions', async () => { + agentNodeType.description.inputs = [ + { + type: connectionType, + maxConnections: 1, + required: true, + }, + ]; + + const closeFunction = jest.fn(); + supplyData.mockResolvedValueOnce({ response, closeFunction }); + + const result = await executeContext.getInputConnectionData(connectionType, 0); + expect(result).toBe(response); + expect(supplyData).toHaveBeenCalled(); + // @ts-expect-error private property + expect(executeContext.closeFunctions).toContain(closeFunction); + }); + }); + + describe(NodeConnectionType.AiTool, () => { + const mockTool = mock(); + const toolNode = mock({ + name: 'Test Tool', + type: 'test.tool', + disabled: false, + }); + const supplyData = jest.fn().mockResolvedValue({ response: mockTool }); + const toolNodeType = mock({ supplyData }); + + const secondToolNode = mock({ name: 'test.secondTool', disabled: false }); + const secondMockTool = mock(); + const secondToolNodeType = mock({ + supplyData: jest.fn().mockResolvedValue({ response: secondMockTool }), + }); + + beforeEach(() => { + nodeTypes.getByNameAndVersion + .calledWith(toolNode.type, expect.anything()) + .mockReturnValue(toolNodeType); + workflow.getParentNodes + .calledWith(agentNode.name, NodeConnectionType.AiTool) + .mockReturnValue([toolNode.name]); + workflow.getNode.calledWith(toolNode.name).mockReturnValue(toolNode); + workflow.getNode.calledWith(secondToolNode.name).mockReturnValue(secondToolNode); + }); + + it('should return empty array when no tools are connected and input is not required', async () => { + agentNodeType.description.inputs = [ + { + type: NodeConnectionType.AiTool, + required: false, + }, + ]; + workflow.getParentNodes.mockReturnValueOnce([]); + + const result = await executeContext.getInputConnectionData(NodeConnectionType.AiTool, 0); + expect(result).toEqual([]); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should throw when required tool node is not connected', async () => { + agentNodeType.description.inputs = [ + { + type: NodeConnectionType.AiTool, + required: true, + }, + ]; + workflow.getParentNodes.mockReturnValueOnce([]); + + await expect( + executeContext.getInputConnectionData(NodeConnectionType.AiTool, 0), + ).rejects.toThrow('must be connected and enabled'); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should handle disabled tool nodes', async () => { + const disabledToolNode = mock({ + name: 'Disabled Tool', + type: 'test.tool', + disabled: true, + }); + + agentNodeType.description.inputs = [ + { + type: NodeConnectionType.AiTool, + required: true, + }, + ]; + + workflow.getParentNodes + .calledWith(agentNode.name, NodeConnectionType.AiTool) + .mockReturnValue([disabledToolNode.name]); + workflow.getNode.calledWith(disabledToolNode.name).mockReturnValue(disabledToolNode); + + await expect( + executeContext.getInputConnectionData(NodeConnectionType.AiTool, 0), + ).rejects.toThrow('must be connected and enabled'); + expect(supplyData).not.toHaveBeenCalled(); + }); + + it('should handle multiple connected tools', async () => { + agentNodeType.description.inputs = [ + { + type: NodeConnectionType.AiTool, + required: true, + }, + ]; + + nodeTypes.getByNameAndVersion + .calledWith(secondToolNode.type, expect.anything()) + .mockReturnValue(secondToolNodeType); + + workflow.getParentNodes + .calledWith(agentNode.name, NodeConnectionType.AiTool) + .mockReturnValue([toolNode.name, secondToolNode.name]); + + const result = await executeContext.getInputConnectionData(NodeConnectionType.AiTool, 0); + expect(result).toEqual([mockTool, secondMockTool]); + expect(supplyData).toHaveBeenCalled(); + expect(secondToolNodeType.supplyData).toHaveBeenCalled(); + }); + + it('should handle tool execution errors', async () => { + supplyData.mockRejectedValueOnce(new Error('Tool execution error')); + + agentNodeType.description.inputs = [ + { + type: NodeConnectionType.AiTool, + required: true, + }, + ]; + + await expect( + executeContext.getInputConnectionData(NodeConnectionType.AiTool, 0), + ).rejects.toThrow(`Error in sub-node ${toolNode.name}`); + expect(supplyData).toHaveBeenCalled(); + }); + + it('should return the tool when there are no issues', async () => { + agentNodeType.description.inputs = [ + { + type: NodeConnectionType.AiTool, + required: true, + }, + ]; + + const result = await executeContext.getInputConnectionData(NodeConnectionType.AiTool, 0); + expect(result).toEqual([mockTool]); + expect(supplyData).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/node-execution-context/__tests__/utils.test.ts b/packages/core/src/node-execution-context/utils/__tests__/validateValueAgainstSchema.test.ts similarity index 59% rename from packages/core/src/node-execution-context/__tests__/utils.test.ts rename to packages/core/src/node-execution-context/utils/__tests__/validateValueAgainstSchema.test.ts index 1871af4c0d..e09299a457 100644 --- a/packages/core/src/node-execution-context/__tests__/utils.test.ts +++ b/packages/core/src/node-execution-context/utils/__tests__/validateValueAgainstSchema.test.ts @@ -1,119 +1,6 @@ -import toPlainObject from 'lodash/toPlainObject'; -import { DateTime } from 'luxon'; -import type { IDataObject, INode, INodeType, NodeParameterValue } from 'n8n-workflow'; -import { ExpressionError } from 'n8n-workflow'; +import type { IDataObject, INode, INodeType } from 'n8n-workflow'; -import { cleanupParameterData, ensureType, validateValueAgainstSchema } from '../utils'; - -describe('cleanupParameterData', () => { - it('should stringify Luxon dates in-place', () => { - const input = { x: 1, y: DateTime.now() as unknown as NodeParameterValue }; - expect(typeof input.y).toBe('object'); - cleanupParameterData(input); - expect(typeof input.y).toBe('string'); - }); - - it('should stringify plain Luxon dates in-place', () => { - const input = { - x: 1, - y: toPlainObject(DateTime.now()), - }; - expect(typeof input.y).toBe('object'); - cleanupParameterData(input); - expect(typeof input.y).toBe('string'); - }); - - it('should handle objects with nameless constructors', () => { - const input = { x: 1, y: { constructor: {} } as NodeParameterValue }; - expect(typeof input.y).toBe('object'); - cleanupParameterData(input); - expect(typeof input.y).toBe('object'); - }); - - it('should handle objects without a constructor', () => { - const input = { x: 1, y: { constructor: undefined } as unknown as NodeParameterValue }; - expect(typeof input.y).toBe('object'); - cleanupParameterData(input); - expect(typeof input.y).toBe('object'); - }); -}); - -describe('ensureType', () => { - it('throws error for null value', () => { - expect(() => ensureType('string', null, 'myParam')).toThrowError( - new ExpressionError("Parameter 'myParam' must not be null"), - ); - }); - - it('throws error for undefined value', () => { - expect(() => ensureType('string', undefined, 'myParam')).toThrowError( - new ExpressionError("Parameter 'myParam' could not be 'undefined'"), - ); - }); - - it('returns string value without modification', () => { - const value = 'hello'; - const expectedValue = value; - const result = ensureType('string', value, 'myParam'); - expect(result).toBe(expectedValue); - }); - - it('returns number value without modification', () => { - const value = 42; - const expectedValue = value; - const result = ensureType('number', value, 'myParam'); - expect(result).toBe(expectedValue); - }); - - it('returns boolean value without modification', () => { - const value = true; - const expectedValue = value; - const result = ensureType('boolean', value, 'myParam'); - expect(result).toBe(expectedValue); - }); - - it('converts object to string if toType is string', () => { - const value = { name: 'John' }; - const expectedValue = JSON.stringify(value); - const result = ensureType('string', value, 'myParam'); - expect(result).toBe(expectedValue); - }); - - it('converts string to number if toType is number', () => { - const value = '10'; - const expectedValue = 10; - const result = ensureType('number', value, 'myParam'); - expect(result).toBe(expectedValue); - }); - - it('throws error for invalid conversion to number', () => { - const value = 'invalid'; - expect(() => ensureType('number', value, 'myParam')).toThrowError( - new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"), - ); - }); - - it('parses valid JSON string to object if toType is object', () => { - const value = '{"name": "Alice"}'; - const expectedValue = JSON.parse(value); - const result = ensureType('object', value, 'myParam'); - expect(result).toEqual(expectedValue); - }); - - it('throws error for invalid JSON string to object conversion', () => { - const value = 'invalid_json'; - expect(() => ensureType('object', value, 'myParam')).toThrowError( - new ExpressionError("Parameter 'myParam' could not be parsed"), - ); - }); - - it('throws error for non-array value if toType is array', () => { - const value = { name: 'Alice' }; - expect(() => ensureType('array', value, 'myParam')).toThrowError( - new ExpressionError("Parameter 'myParam' must be an array, but we got object"), - ); - }); -}); +import { validateValueAgainstSchema } from '../validateValueAgainstSchema'; describe('validateValueAgainstSchema', () => { test('should validate fixedCollection values parameter', () => { diff --git a/packages/core/src/node-execution-context/utils/cleanupParameterData.ts b/packages/core/src/node-execution-context/utils/cleanupParameterData.ts new file mode 100644 index 0000000000..59f27874ff --- /dev/null +++ b/packages/core/src/node-execution-context/utils/cleanupParameterData.ts @@ -0,0 +1,31 @@ +import { DateTime } from 'luxon'; +import type { INodeParameters, NodeParameterValueType } from 'n8n-workflow'; + +/** + * Clean up parameter data to make sure that only valid data gets returned + * INFO: Currently only converts Luxon Dates as we know for sure it will not be breaking + */ +export function cleanupParameterData(inputData: NodeParameterValueType): void { + if (typeof inputData !== 'object' || inputData === null) { + return; + } + + if (Array.isArray(inputData)) { + inputData.forEach((value) => cleanupParameterData(value as NodeParameterValueType)); + return; + } + + if (typeof inputData === 'object') { + Object.keys(inputData).forEach((key) => { + const value = (inputData as INodeParameters)[key]; + if (typeof value === 'object') { + if (DateTime.isDateTime(value)) { + // Is a special luxon date so convert to string + (inputData as INodeParameters)[key] = value.toString(); + } else { + cleanupParameterData(value); + } + } + }); + } +} diff --git a/packages/core/src/node-execution-context/utils/ensureType.ts b/packages/core/src/node-execution-context/utils/ensureType.ts new file mode 100644 index 0000000000..4869c73ba4 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/ensureType.ts @@ -0,0 +1,103 @@ +import type { EnsureTypeOptions } from 'n8n-workflow'; +import { ExpressionError } from 'n8n-workflow'; + +export function ensureType( + toType: EnsureTypeOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameterValue: any, + parameterName: string, + errorOptions?: { itemIndex?: number; runIndex?: number; nodeCause?: string }, +): string | number | boolean | object { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + let returnData = parameterValue; + + if (returnData === null) { + throw new ExpressionError(`Parameter '${parameterName}' must not be null`, errorOptions); + } + + if (returnData === undefined) { + throw new ExpressionError( + `Parameter '${parameterName}' could not be 'undefined'`, + errorOptions, + ); + } + + if (['object', 'array', 'json'].includes(toType)) { + if (typeof returnData !== 'object') { + // if value is not an object and is string try to parse it, else throw an error + if (typeof returnData === 'string' && returnData.length) { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsedValue = JSON.parse(returnData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + returnData = parsedValue; + } catch (error) { + throw new ExpressionError(`Parameter '${parameterName}' could not be parsed`, { + ...errorOptions, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + description: error.message, + }); + } + } else { + throw new ExpressionError( + `Parameter '${parameterName}' must be an ${toType}, but we got '${String(parameterValue)}'`, + errorOptions, + ); + } + } else if (toType === 'json') { + // value is an object, make sure it is valid JSON + try { + JSON.stringify(returnData); + } catch (error) { + throw new ExpressionError(`Parameter '${parameterName}' is not valid JSON`, { + ...errorOptions, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + description: error.message, + }); + } + } + + if (toType === 'array' && !Array.isArray(returnData)) { + // value is not an array, but has to be + throw new ExpressionError( + `Parameter '${parameterName}' must be an array, but we got object`, + errorOptions, + ); + } + } + + try { + if (toType === 'string') { + if (typeof returnData === 'object') { + returnData = JSON.stringify(returnData); + } else { + returnData = String(returnData); + } + } + + if (toType === 'number') { + returnData = Number(returnData); + if (Number.isNaN(returnData)) { + throw new ExpressionError( + `Parameter '${parameterName}' must be a number, but we got '${parameterValue}'`, + errorOptions, + ); + } + } + + if (toType === 'boolean') { + returnData = Boolean(returnData); + } + } catch (error) { + if (error instanceof ExpressionError) throw error; + + throw new ExpressionError(`Parameter '${parameterName}' could not be converted to ${toType}`, { + ...errorOptions, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + description: error.message, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return returnData; +} diff --git a/packages/core/src/node-execution-context/utils/getAdditionalKeys.ts b/packages/core/src/node-execution-context/utils/getAdditionalKeys.ts new file mode 100644 index 0000000000..28bf3b89f6 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/getAdditionalKeys.ts @@ -0,0 +1,74 @@ +import type { + IRunExecutionData, + IWorkflowDataProxyAdditionalKeys, + IWorkflowExecuteAdditionalData, + WorkflowExecuteMode, +} from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; + +import { PLACEHOLDER_EMPTY_EXECUTION_ID } from '@/Constants'; +import { + setWorkflowExecutionMetadata, + setAllWorkflowExecutionMetadata, + getWorkflowExecutionMetadata, + getAllWorkflowExecutionMetadata, +} from '@/ExecutionMetadata'; +import { getSecretsProxy } from '@/Secrets'; + +/** Returns the additional keys for Expressions and Function-Nodes */ +export function getAdditionalKeys( + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + runExecutionData: IRunExecutionData | null, + options?: { secretsEnabled?: boolean }, +): IWorkflowDataProxyAdditionalKeys { + const executionId = additionalData.executionId ?? PLACEHOLDER_EMPTY_EXECUTION_ID; + const resumeUrl = `${additionalData.webhookWaitingBaseUrl}/${executionId}`; + const resumeFormUrl = `${additionalData.formWaitingBaseUrl}/${executionId}`; + return { + $execution: { + id: executionId, + mode: mode === 'manual' ? 'test' : 'production', + resumeUrl, + resumeFormUrl, + customData: runExecutionData + ? { + set(key: string, value: string): void { + try { + setWorkflowExecutionMetadata(runExecutionData, key, value); + } catch (e) { + if (mode === 'manual') { + throw e; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + LoggerProxy.debug(e.message); + } + }, + setAll(obj: Record): void { + try { + setAllWorkflowExecutionMetadata(runExecutionData, obj); + } catch (e) { + if (mode === 'manual') { + throw e; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + LoggerProxy.debug(e.message); + } + }, + get(key: string): string { + return getWorkflowExecutionMetadata(runExecutionData, key); + }, + getAll(): Record { + return getAllWorkflowExecutionMetadata(runExecutionData); + }, + } + : undefined, + }, + $vars: additionalData.variables, + $secrets: options?.secretsEnabled ? getSecretsProxy(additionalData) : undefined, + + // deprecated + $executionId: executionId, + $resumeWebhookUrl: resumeUrl, + }; +} diff --git a/packages/core/src/node-execution-context/utils/getInputConnectionData.ts b/packages/core/src/node-execution-context/utils/getInputConnectionData.ts new file mode 100644 index 0000000000..231f672d65 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/getInputConnectionData.ts @@ -0,0 +1,184 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { + CloseFunction, + IExecuteData, + IExecuteFunctions, + INodeExecutionData, + IRunExecutionData, + ITaskDataConnections, + IWorkflowExecuteAdditionalData, + Workflow, + WorkflowExecuteMode, + SupplyData, + AINodeConnectionType, +} from 'n8n-workflow'; +import { + NodeConnectionType, + NodeOperationError, + ExecutionBaseError, + ApplicationError, +} from 'n8n-workflow'; + +import { createNodeAsTool } from '@/CreateNodeAsTool'; +// eslint-disable-next-line import/no-cycle +import { SupplyDataContext } from '@/node-execution-context'; +import type { ExecuteContext, WebhookContext } from '@/node-execution-context'; + +export async function getInputConnectionData( + this: ExecuteContext | WebhookContext | SupplyDataContext, + workflow: Workflow, + runExecutionData: IRunExecutionData, + parentRunIndex: number, + connectionInputData: INodeExecutionData[], + parentInputData: ITaskDataConnections, + additionalData: IWorkflowExecuteAdditionalData, + executeData: IExecuteData, + mode: WorkflowExecuteMode, + closeFunctions: CloseFunction[], + connectionType: AINodeConnectionType, + itemIndex: number, + abortSignal?: AbortSignal, +): Promise { + const parentNode = this.getNode(); + + const inputConfiguration = this.nodeInputs.find((input) => input.type === connectionType); + if (inputConfiguration === undefined) { + throw new ApplicationError('Node does not have input of type', { + extra: { nodeName: parentNode.name, connectionType }, + }); + } + + const connectedNodes = this.getConnectedNodes(connectionType); + if (connectedNodes.length === 0) { + if (inputConfiguration.required) { + throw new NodeOperationError( + parentNode, + `A ${inputConfiguration?.displayName ?? connectionType} sub-node must be connected and enabled`, + ); + } + return inputConfiguration.maxConnections === 1 ? undefined : []; + } + + if ( + inputConfiguration.maxConnections !== undefined && + connectedNodes.length > inputConfiguration.maxConnections + ) { + throw new NodeOperationError( + parentNode, + `Only ${inputConfiguration.maxConnections} ${connectionType} sub-nodes are/is allowed to be connected`, + ); + } + + const nodes: SupplyData[] = []; + for (const connectedNode of connectedNodes) { + const connectedNodeType = workflow.nodeTypes.getByNameAndVersion( + connectedNode.type, + connectedNode.typeVersion, + ); + const contextFactory = (runIndex: number, inputData: ITaskDataConnections) => + new SupplyDataContext( + workflow, + connectedNode, + additionalData, + mode, + runExecutionData, + runIndex, + connectionInputData, + inputData, + connectionType, + executeData, + closeFunctions, + abortSignal, + ); + + if (!connectedNodeType.supplyData) { + if (connectedNodeType.description.outputs.includes(NodeConnectionType.AiTool)) { + /** + * This keeps track of how many times this specific AI tool node has been invoked. + * It is incremented on every invocation of the tool to keep the output of each invocation separate from each other. + */ + let toolRunIndex = 0; + const supplyData = createNodeAsTool({ + node: connectedNode, + nodeType: connectedNodeType, + handleToolInvocation: async (toolArgs) => { + const runIndex = toolRunIndex++; + const context = contextFactory(runIndex, {}); + context.addInputData(NodeConnectionType.AiTool, [[{ json: toolArgs }]]); + + try { + // Execute the sub-node with the proxied context + const result = await connectedNodeType.execute?.call( + context as unknown as IExecuteFunctions, + ); + + // Process and map the results + const mappedResults = result?.[0]?.flatMap((item) => item.json); + + // Add output data to the context + context.addOutputData(NodeConnectionType.AiTool, runIndex, [ + [{ json: { response: mappedResults } }], + ]); + + // Return the stringified results + return JSON.stringify(mappedResults); + } catch (error) { + const nodeError = new NodeOperationError(connectedNode, error as Error); + context.addOutputData(NodeConnectionType.AiTool, runIndex, nodeError); + return 'Error during node execution: ' + nodeError.description; + } + }, + }); + nodes.push(supplyData); + } else { + throw new ApplicationError('Node does not have a `supplyData` method defined', { + extra: { nodeName: connectedNode.name }, + }); + } + } else { + const context = contextFactory(parentRunIndex, parentInputData); + try { + const supplyData = await connectedNodeType.supplyData.call(context, itemIndex); + if (supplyData.closeFunction) { + closeFunctions.push(supplyData.closeFunction); + } + nodes.push(supplyData); + } catch (error) { + // Propagate errors from sub-nodes + if (error instanceof ExecutionBaseError) { + if (error.functionality === 'configuration-node') throw error; + } else { + error = new NodeOperationError(connectedNode, error, { + itemIndex, + }); + } + + let currentNodeRunIndex = 0; + if (runExecutionData.resultData.runData.hasOwnProperty(parentNode.name)) { + currentNodeRunIndex = runExecutionData.resultData.runData[parentNode.name].length; + } + + // Display the error on the node which is causing it + await context.addExecutionDataFunctions( + 'input', + error, + connectionType, + parentNode.name, + currentNodeRunIndex, + ); + + // Display on the calling node which node has the error + throw new NodeOperationError(connectedNode, `Error in sub-node ${connectedNode.name}`, { + itemIndex, + functionality: 'configuration-node', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + description: error.message, + }); + } + } + } + + return inputConfiguration.maxConnections === 1 + ? (nodes || [])[0]?.response + : nodes.map((node) => node.response); +} diff --git a/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts b/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts new file mode 100644 index 0000000000..2792199807 --- /dev/null +++ b/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts @@ -0,0 +1,218 @@ +import type { + FieldType, + IDataObject, + INode, + INodeProperties, + INodePropertyCollection, + INodePropertyOptions, + INodeType, +} from 'n8n-workflow'; +import { + ExpressionError, + isResourceMapperValue, + NodeHelpers, + validateFieldType, +} from 'n8n-workflow'; + +import type { ExtendedValidationResult } from '@/Interfaces'; + +const validateResourceMapperValue = ( + parameterName: string, + paramValues: { [key: string]: unknown }, + node: INode, + skipRequiredCheck = false, +): ExtendedValidationResult => { + const result: ExtendedValidationResult = { valid: true, newValue: paramValues }; + const paramNameParts = parameterName.split('.'); + if (paramNameParts.length !== 2) { + return result; + } + const resourceMapperParamName = paramNameParts[0]; + const resourceMapperField = node.parameters[resourceMapperParamName]; + if (!resourceMapperField || !isResourceMapperValue(resourceMapperField)) { + return result; + } + const schema = resourceMapperField.schema; + const paramValueNames = Object.keys(paramValues); + for (let i = 0; i < paramValueNames.length; i++) { + const key = paramValueNames[i]; + const resolvedValue = paramValues[key]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call + const schemaEntry = schema.find((s) => s.id === key); + + if ( + !skipRequiredCheck && + schemaEntry?.required === true && + schemaEntry.type !== 'boolean' && + !resolvedValue + ) { + return { + valid: false, + errorMessage: `The value "${String(key)}" is required but not set`, + fieldName: key, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (schemaEntry?.type) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + valueOptions: schemaEntry.options, + }); + if (!validationResult.valid) { + return { ...validationResult, fieldName: key }; + } else { + // If it's valid, set the casted value + paramValues[key] = validationResult.newValue; + } + } + } + return result; +}; + +const validateCollection = ( + node: INode, + runIndex: number, + itemIndex: number, + propertyDescription: INodeProperties, + parameterPath: string[], + validationResult: ExtendedValidationResult, +): ExtendedValidationResult => { + let nestedDescriptions: INodeProperties[] | undefined; + + if (propertyDescription.type === 'fixedCollection') { + nestedDescriptions = (propertyDescription.options as INodePropertyCollection[]).find( + (entry) => entry.name === parameterPath[1], + )?.values; + } + + if (propertyDescription.type === 'collection') { + nestedDescriptions = propertyDescription.options as INodeProperties[]; + } + + if (!nestedDescriptions) { + return validationResult; + } + + const validationMap: { + [key: string]: { type: FieldType; displayName: string; options?: INodePropertyOptions[] }; + } = {}; + + for (const prop of nestedDescriptions) { + if (!prop.validateType || prop.ignoreValidationDuringExecution) continue; + + validationMap[prop.name] = { + type: prop.validateType, + displayName: prop.displayName, + options: + prop.validateType === 'options' ? (prop.options as INodePropertyOptions[]) : undefined, + }; + } + + if (!Object.keys(validationMap).length) { + return validationResult; + } + + if (validationResult.valid) { + for (const value of Array.isArray(validationResult.newValue) + ? (validationResult.newValue as IDataObject[]) + : [validationResult.newValue as IDataObject]) { + for (const key of Object.keys(value)) { + if (!validationMap[key]) continue; + + const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, { + valueOptions: validationMap[key].options, + }); + + if (!fieldValidationResult.valid) { + throw new ExpressionError( + `Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`, + { + description: fieldValidationResult.errorMessage, + runIndex, + itemIndex, + nodeCause: node.name, + }, + ); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + value[key] = fieldValidationResult.newValue; + } + } + } + + return validationResult; +}; + +export const validateValueAgainstSchema = ( + node: INode, + nodeType: INodeType, + parameterValue: string | number | boolean | object | null | undefined, + parameterName: string, + runIndex: number, + itemIndex: number, +) => { + const parameterPath = parameterName.split('.'); + + const propertyDescription = nodeType.description.properties.find( + (prop) => + parameterPath[0] === prop.name && NodeHelpers.displayParameter(node.parameters, prop, node), + ); + + if (!propertyDescription) { + return parameterValue; + } + + let validationResult: ExtendedValidationResult = { valid: true, newValue: parameterValue }; + + if ( + parameterPath.length === 1 && + propertyDescription.validateType && + !propertyDescription.ignoreValidationDuringExecution + ) { + validationResult = validateFieldType( + parameterName, + parameterValue, + propertyDescription.validateType, + ); + } else if ( + propertyDescription.type === 'resourceMapper' && + parameterPath[1] === 'value' && + typeof parameterValue === 'object' + ) { + validationResult = validateResourceMapperValue( + parameterName, + parameterValue as { [key: string]: unknown }, + node, + propertyDescription.typeOptions?.resourceMapper?.mode !== 'add', + ); + } else if (['fixedCollection', 'collection'].includes(propertyDescription.type)) { + validationResult = validateCollection( + node, + runIndex, + itemIndex, + propertyDescription, + parameterPath, + validationResult, + ); + } + + if (!validationResult.valid) { + throw new ExpressionError( + `Invalid input for '${ + validationResult.fieldName + ? String(validationResult.fieldName) + : propertyDescription.displayName + }' [item ${itemIndex}]`, + { + description: validationResult.errorMessage, + runIndex, + itemIndex, + nodeCause: node.name, + }, + ); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return validationResult.newValue; +}; diff --git a/packages/core/src/node-execution-context/webhook-context.ts b/packages/core/src/node-execution-context/webhook-context.ts index 04d1df5e40..9d131a4103 100644 --- a/packages/core/src/node-execution-context/webhook-context.ts +++ b/packages/core/src/node-execution-context/webhook-context.ts @@ -22,13 +22,13 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow'; import { copyBinaryFile, getBinaryHelperFunctions, - getInputConnectionData, getNodeWebhookUrl, getRequestHelperFunctions, returnJsonArray, } from '@/NodeExecuteFunctions'; import { NodeExecutionContext } from './node-execution-context'; +import { getInputConnectionData } from './utils/getInputConnectionData'; export class WebhookContext extends NodeExecutionContext implements IWebhookFunctions { readonly helpers: IWebhookFunctions['helpers']; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index c5fa0a938a..c5df420e29 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1992,7 +1992,28 @@ export interface IWorkflowDataProxyData { constructor: any; } -export type IWorkflowDataProxyAdditionalKeys = IDataObject; +export type IWorkflowDataProxyAdditionalKeys = IDataObject & { + $execution?: { + id: string; + mode: 'test' | 'production'; + resumeUrl: string; + resumeFormUrl: string; + customData?: { + set(key: string, value: string): void; + setAll(obj: Record): void; + get(key: string): string; + getAll(): Record; + }; + }; + $vars?: IDataObject; + $secrets?: IDataObject; + $pageCount?: number; + + /** @deprecated */ + $executionId?: string; + /** @deprecated */ + $resumeWebhookUrl?: string; +}; export interface IWorkflowMetadata { id?: string; From f924f2a6d736e33ab5fc12cbac6cba27340839db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 20 Dec 2024 15:25:33 +0100 Subject: [PATCH 02/66] fix(core): Register workflows as active only after all of the triggers and pollers setup successfully (#12244) --- packages/core/src/ActiveWorkflows.ts | 21 +- .../src/__tests__/ActiveWorkflows.test.ts | 290 ++++++++++++++++++ packages/core/test/TriggersAndPollers.test.ts | 138 +++++---- 3 files changed, 373 insertions(+), 76 deletions(-) create mode 100644 packages/core/src/__tests__/ActiveWorkflows.test.ts diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index b7604f9778..e3ca8614c2 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -71,16 +71,13 @@ export class ActiveWorkflows { getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions, ) { - this.activeWorkflows[workflowId] = {}; const triggerNodes = workflow.getTriggerNodes(); - let triggerResponse: ITriggerResponse | undefined; - - this.activeWorkflows[workflowId].triggerResponses = []; + const triggerResponses: ITriggerResponse[] = []; for (const triggerNode of triggerNodes) { try { - triggerResponse = await this.triggersAndPollers.runTrigger( + const triggerResponse = await this.triggersAndPollers.runTrigger( workflow, triggerNode, getTriggerFunctions, @@ -89,10 +86,7 @@ export class ActiveWorkflows { activation, ); if (triggerResponse !== undefined) { - // If a response was given save it - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - this.activeWorkflows[workflowId].triggerResponses!.push(triggerResponse); + triggerResponses.push(triggerResponse); } } catch (e) { const error = e instanceof Error ? e : new Error(`${e}`); @@ -104,6 +98,8 @@ export class ActiveWorkflows { } } + this.activeWorkflows[workflowId] = { triggerResponses }; + const pollingNodes = workflow.getPollNodes(); if (pollingNodes.length === 0) return; @@ -119,6 +115,11 @@ export class ActiveWorkflows { activation, ); } catch (e) { + // Do not mark this workflow as active if there are no triggerResponses, and any polling activation failed + if (triggerResponses.length === 0) { + delete this.activeWorkflows[workflowId]; + } + const error = e instanceof Error ? e : new Error(`${e}`); throw new WorkflowActivationError( @@ -132,7 +133,7 @@ export class ActiveWorkflows { /** * Activates polling for the given node */ - async activatePolling( + private async activatePolling( node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, diff --git a/packages/core/src/__tests__/ActiveWorkflows.test.ts b/packages/core/src/__tests__/ActiveWorkflows.test.ts new file mode 100644 index 0000000000..85487a0cec --- /dev/null +++ b/packages/core/src/__tests__/ActiveWorkflows.test.ts @@ -0,0 +1,290 @@ +import { mock } from 'jest-mock-extended'; +import type { + IGetExecuteTriggerFunctions, + INode, + ITriggerResponse, + IWorkflowExecuteAdditionalData, + Workflow, + WorkflowActivateMode, + WorkflowExecuteMode, + TriggerTime, + CronExpression, +} from 'n8n-workflow'; +import { LoggerProxy, TriggerCloseError, WorkflowActivationError } from 'n8n-workflow'; + +import { ActiveWorkflows } from '@/ActiveWorkflows'; +import type { ErrorReporter } from '@/error-reporter'; +import type { PollContext } from '@/node-execution-context'; +import type { ScheduledTaskManager } from '@/ScheduledTaskManager'; +import type { TriggersAndPollers } from '@/TriggersAndPollers'; + +describe('ActiveWorkflows', () => { + const workflowId = 'test-workflow-id'; + const workflow = mock(); + const additionalData = mock(); + const mode: WorkflowExecuteMode = 'trigger'; + const activation: WorkflowActivateMode = 'init'; + + const getTriggerFunctions = jest.fn() as IGetExecuteTriggerFunctions; + const triggerResponse = mock(); + + const pollFunctions = mock(); + const getPollFunctions = jest.fn(); + + LoggerProxy.init(mock()); + const scheduledTaskManager = mock(); + const triggersAndPollers = mock(); + const errorReporter = mock(); + const triggerNode = mock(); + const pollNode = mock(); + + let activeWorkflows: ActiveWorkflows; + + beforeEach(() => { + jest.clearAllMocks(); + activeWorkflows = new ActiveWorkflows(scheduledTaskManager, triggersAndPollers, errorReporter); + }); + + type PollTimes = { item: TriggerTime[] }; + type TestOptions = { + triggerNodes?: INode[]; + pollNodes?: INode[]; + triggerError?: Error; + pollError?: Error; + pollTimes?: PollTimes; + }; + + const addWorkflow = async ({ + triggerNodes = [], + pollNodes = [], + triggerError, + pollError, + pollTimes = { item: [{ mode: 'everyMinute' }] }, + }: TestOptions) => { + workflow.getTriggerNodes.mockReturnValue(triggerNodes); + workflow.getPollNodes.mockReturnValue(pollNodes); + pollFunctions.getNodeParameter.calledWith('pollTimes').mockReturnValue(pollTimes); + + if (triggerError) { + triggersAndPollers.runTrigger.mockRejectedValueOnce(triggerError); + } else { + triggersAndPollers.runTrigger.mockResolvedValue(triggerResponse); + } + + if (pollError) { + triggersAndPollers.runPoll.mockRejectedValueOnce(pollError); + } else { + getPollFunctions.mockReturnValue(pollFunctions); + } + + return await activeWorkflows.add( + workflowId, + workflow, + additionalData, + mode, + activation, + getTriggerFunctions, + getPollFunctions, + ); + }; + + describe('add()', () => { + describe('should activate workflow', () => { + it('with trigger nodes', async () => { + await addWorkflow({ triggerNodes: [triggerNode] }); + + expect(activeWorkflows.isActive(workflowId)).toBe(true); + expect(workflow.getTriggerNodes).toHaveBeenCalled(); + expect(triggersAndPollers.runTrigger).toHaveBeenCalledWith( + workflow, + triggerNode, + getTriggerFunctions, + additionalData, + mode, + activation, + ); + }); + + it('with polling nodes', async () => { + await addWorkflow({ pollNodes: [pollNode] }); + + expect(activeWorkflows.isActive(workflowId)).toBe(true); + expect(workflow.getPollNodes).toHaveBeenCalled(); + expect(scheduledTaskManager.registerCron).toHaveBeenCalled(); + }); + + it('with both trigger and polling nodes', async () => { + await addWorkflow({ triggerNodes: [triggerNode], pollNodes: [pollNode] }); + + expect(activeWorkflows.isActive(workflowId)).toBe(true); + expect(workflow.getTriggerNodes).toHaveBeenCalled(); + expect(workflow.getPollNodes).toHaveBeenCalled(); + expect(triggersAndPollers.runTrigger).toHaveBeenCalledWith( + workflow, + triggerNode, + getTriggerFunctions, + additionalData, + mode, + activation, + ); + expect(scheduledTaskManager.registerCron).toHaveBeenCalled(); + expect(triggersAndPollers.runPoll).toHaveBeenCalledWith(workflow, pollNode, pollFunctions); + }); + }); + + describe('should throw error', () => { + it('if trigger activation fails', async () => { + const error = new Error('Trigger activation failed'); + await expect( + addWorkflow({ triggerNodes: [triggerNode], triggerError: error }), + ).rejects.toThrow(WorkflowActivationError); + expect(activeWorkflows.isActive(workflowId)).toBe(false); + }); + + it('if polling activation fails', async () => { + const error = new Error('Failed to activate polling'); + await expect(addWorkflow({ pollNodes: [pollNode], pollError: error })).rejects.toThrow( + WorkflowActivationError, + ); + expect(activeWorkflows.isActive(workflowId)).toBe(false); + }); + + it('if the polling interval is too short', async () => { + const pollTimes: PollTimes = { + item: [ + { + mode: 'custom', + cronExpression: '* * * * *' as CronExpression, + }, + ], + }; + + await expect(addWorkflow({ pollNodes: [pollNode], pollTimes })).rejects.toThrow( + 'The polling interval is too short. It has to be at least a minute.', + ); + + expect(scheduledTaskManager.registerCron).not.toHaveBeenCalled(); + }); + }); + + describe('should handle polling errors', () => { + it('should throw error when poll fails during initial testing', async () => { + const error = new Error('Poll function failed'); + + await expect(addWorkflow({ pollNodes: [pollNode], pollError: error })).rejects.toThrow( + WorkflowActivationError, + ); + + expect(triggersAndPollers.runPoll).toHaveBeenCalledWith(workflow, pollNode, pollFunctions); + expect(pollFunctions.__emit).not.toHaveBeenCalled(); + expect(pollFunctions.__emitError).not.toHaveBeenCalled(); + }); + + it('should emit error when poll fails during regular polling', async () => { + const error = new Error('Poll function failed'); + triggersAndPollers.runPoll + .mockResolvedValueOnce(null) // Succeed on first call (testing) + .mockRejectedValueOnce(error); // Fail on second call (regular polling) + + await addWorkflow({ pollNodes: [pollNode] }); + + // Get the executeTrigger function that was registered + const registerCronCall = scheduledTaskManager.registerCron.mock.calls[0]; + const executeTrigger = registerCronCall[2] as () => Promise; + + // Execute the trigger function to simulate a regular poll + await executeTrigger(); + + expect(triggersAndPollers.runPoll).toHaveBeenCalledTimes(2); + expect(pollFunctions.__emit).not.toHaveBeenCalled(); + expect(pollFunctions.__emitError).toHaveBeenCalledWith(error); + }); + }); + }); + + describe('remove()', () => { + const setupForRemoval = async () => { + await addWorkflow({ triggerNodes: [triggerNode] }); + return await activeWorkflows.remove(workflowId); + }; + + it('should remove an active workflow', async () => { + const result = await setupForRemoval(); + + expect(result).toBe(true); + expect(activeWorkflows.isActive(workflowId)).toBe(false); + expect(scheduledTaskManager.deregisterCrons).toHaveBeenCalledWith(workflowId); + expect(triggerResponse.closeFunction).toHaveBeenCalled(); + }); + + it('should return false when removing non-existent workflow', async () => { + const result = await activeWorkflows.remove('non-existent'); + + expect(result).toBe(false); + expect(scheduledTaskManager.deregisterCrons).not.toHaveBeenCalled(); + }); + + it('should handle TriggerCloseError when closing trigger', async () => { + const triggerCloseError = new TriggerCloseError(triggerNode, { level: 'warning' }); + (triggerResponse.closeFunction as jest.Mock).mockRejectedValueOnce(triggerCloseError); + + const result = await setupForRemoval(); + + expect(result).toBe(true); + expect(activeWorkflows.isActive(workflowId)).toBe(false); + expect(triggerResponse.closeFunction).toHaveBeenCalled(); + expect(errorReporter.error).toHaveBeenCalledWith(triggerCloseError, { + extra: { workflowId }, + }); + }); + + it('should throw WorkflowDeactivationError when closeFunction throws regular error', async () => { + const error = new Error('Close function failed'); + (triggerResponse.closeFunction as jest.Mock).mockRejectedValueOnce(error); + + await addWorkflow({ triggerNodes: [triggerNode] }); + + await expect(activeWorkflows.remove(workflowId)).rejects.toThrow( + `Failed to deactivate trigger of workflow ID "${workflowId}": "Close function failed"`, + ); + + expect(triggerResponse.closeFunction).toHaveBeenCalled(); + expect(errorReporter.error).not.toHaveBeenCalled(); + }); + }); + + describe('get() and isActive()', () => { + it('should return workflow data for active workflow', async () => { + await addWorkflow({ triggerNodes: [triggerNode] }); + + expect(activeWorkflows.isActive(workflowId)).toBe(true); + expect(activeWorkflows.get(workflowId)).toBeDefined(); + }); + + it('should return undefined for non-active workflow', () => { + expect(activeWorkflows.isActive('non-existent')).toBe(false); + expect(activeWorkflows.get('non-existent')).toBeUndefined(); + }); + }); + + describe('allActiveWorkflows()', () => { + it('should return all active workflow IDs', async () => { + await addWorkflow({ triggerNodes: [triggerNode] }); + + const activeIds = activeWorkflows.allActiveWorkflows(); + + expect(activeIds).toEqual([workflowId]); + }); + }); + + describe('removeAllTriggerAndPollerBasedWorkflows()', () => { + it('should remove all active workflows', async () => { + await addWorkflow({ triggerNodes: [triggerNode] }); + + await activeWorkflows.removeAllTriggerAndPollerBasedWorkflows(); + + expect(activeWorkflows.allActiveWorkflows()).toEqual([]); + expect(scheduledTaskManager.deregisterCrons).toHaveBeenCalledWith(workflowId); + }); + }); +}); diff --git a/packages/core/test/TriggersAndPollers.test.ts b/packages/core/test/TriggersAndPollers.test.ts index c30a0693a6..27cc8b47d9 100644 --- a/packages/core/test/TriggersAndPollers.test.ts +++ b/packages/core/test/TriggersAndPollers.test.ts @@ -9,6 +9,8 @@ import type { INodeType, INodeTypes, ITriggerFunctions, + WorkflowHooks, + IRun, } from 'n8n-workflow'; import { TriggersAndPollers } from '@/TriggersAndPollers'; @@ -21,11 +23,13 @@ describe('TriggersAndPollers', () => { }); const nodeTypes = mock(); const workflow = mock({ nodeTypes }); + const hookFunctions = mock({ + sendResponse: [], + workflowExecuteAfter: [], + }); const additionalData = mock({ hooks: { - hookFunctions: { - sendResponse: [], - }, + hookFunctions, }, }); const triggersAndPollers = new TriggersAndPollers(); @@ -39,87 +43,80 @@ describe('TriggersAndPollers', () => { const triggerFunctions = mock(); const getTriggerFunctions = jest.fn().mockReturnValue(triggerFunctions); const triggerFn = jest.fn(); + const mockEmitData: INodeExecutionData[][] = [[{ json: { data: 'test' } }]]; + + const runTriggerHelper = async (mode: 'manual' | 'trigger' = 'trigger') => + await triggersAndPollers.runTrigger( + workflow, + node, + getTriggerFunctions, + additionalData, + mode, + 'init', + ); it('should throw error if node type does not have trigger function', async () => { - await expect( - triggersAndPollers.runTrigger( - workflow, - node, - getTriggerFunctions, - additionalData, - 'trigger', - 'init', - ), - ).rejects.toThrow(ApplicationError); + await expect(runTriggerHelper()).rejects.toThrow(ApplicationError); }); it('should call trigger function in regular mode', async () => { nodeType.trigger = triggerFn; triggerFn.mockResolvedValue({ test: true }); - const result = await triggersAndPollers.runTrigger( - workflow, - node, - getTriggerFunctions, - additionalData, - 'trigger', - 'init', - ); + const result = await runTriggerHelper(); expect(triggerFn).toHaveBeenCalled(); expect(result).toEqual({ test: true }); }); - it('should handle manual mode with promise resolution', async () => { - const mockEmitData: INodeExecutionData[][] = [[{ json: { data: 'test' } }]]; - const mockTriggerResponse = { workflowId: '123' }; + describe('manual mode', () => { + const getMockTriggerFunctions = () => getTriggerFunctions.mock.results[0]?.value; - nodeType.trigger = triggerFn; - triggerFn.mockResolvedValue(mockTriggerResponse); + beforeEach(() => { + nodeType.trigger = triggerFn; + triggerFn.mockResolvedValue({ workflowId: '123' }); + }); - const result = await triggersAndPollers.runTrigger( - workflow, - node, - getTriggerFunctions, - additionalData, - 'manual', - 'init', - ); + it('should handle promise resolution', async () => { + const result = await runTriggerHelper('manual'); - expect(result).toBeDefined(); - expect(result?.manualTriggerResponse).toBeInstanceOf(Promise); + expect(result?.manualTriggerResponse).toBeInstanceOf(Promise); + getMockTriggerFunctions()?.emit?.(mockEmitData); + }); - // Simulate emit - const mockTriggerFunctions = getTriggerFunctions.mock.results[0]?.value; - if (mockTriggerFunctions?.emit) { - mockTriggerFunctions.emit(mockEmitData); - } - }); + it('should handle error emission', async () => { + const testError = new Error('Test error'); + const result = await runTriggerHelper('manual'); - it('should handle error emission in manual mode', async () => { - const testError = new Error('Test error'); + getMockTriggerFunctions()?.emitError?.(testError); + await expect(result?.manualTriggerResponse).rejects.toThrow(testError); + }); - nodeType.trigger = triggerFn; - triggerFn.mockResolvedValue({}); + it('should handle response promise', async () => { + const responsePromise = { resolve: jest.fn(), reject: jest.fn() }; + await runTriggerHelper('manual'); - const result = await triggersAndPollers.runTrigger( - workflow, - node, - getTriggerFunctions, - additionalData, - 'manual', - 'init', - ); + getMockTriggerFunctions()?.emit?.(mockEmitData, responsePromise); - expect(result?.manualTriggerResponse).toBeInstanceOf(Promise); + expect(hookFunctions.sendResponse?.length).toBe(1); + await hookFunctions.sendResponse![0]?.({ testResponse: true }); + expect(responsePromise.resolve).toHaveBeenCalledWith({ testResponse: true }); + }); - // Simulate error - const mockTriggerFunctions = getTriggerFunctions.mock.results[0]?.value; - if (mockTriggerFunctions?.emitError) { - mockTriggerFunctions.emitError(testError); - } + it('should handle both response and done promises', async () => { + const responsePromise = { resolve: jest.fn(), reject: jest.fn() }; + const donePromise = { resolve: jest.fn(), reject: jest.fn() }; + const mockRunData = mock({ data: { resultData: { runData: {} } } }); - await expect(result?.manualTriggerResponse).rejects.toThrow(testError); + await runTriggerHelper('manual'); + getMockTriggerFunctions()?.emit?.(mockEmitData, responsePromise, donePromise); + + await hookFunctions.sendResponse![0]?.({ testResponse: true }); + expect(responsePromise.resolve).toHaveBeenCalledWith({ testResponse: true }); + + await hookFunctions.workflowExecuteAfter?.[0]?.(mockRunData, {}); + expect(donePromise.resolve).toHaveBeenCalledWith(mockRunData); + }); }); }); @@ -127,10 +124,11 @@ describe('TriggersAndPollers', () => { const pollFunctions = mock(); const pollFn = jest.fn(); + const runPollHelper = async () => + await triggersAndPollers.runPoll(workflow, node, pollFunctions); + it('should throw error if node type does not have poll function', async () => { - await expect(triggersAndPollers.runPoll(workflow, node, pollFunctions)).rejects.toThrow( - ApplicationError, - ); + await expect(runPollHelper()).rejects.toThrow(ApplicationError); }); it('should call poll function and return result', async () => { @@ -138,7 +136,7 @@ describe('TriggersAndPollers', () => { nodeType.poll = pollFn; pollFn.mockResolvedValue(mockPollResult); - const result = await triggersAndPollers.runPoll(workflow, node, pollFunctions); + const result = await runPollHelper(); expect(pollFn).toHaveBeenCalled(); expect(result).toBe(mockPollResult); @@ -148,10 +146,18 @@ describe('TriggersAndPollers', () => { nodeType.poll = pollFn; pollFn.mockResolvedValue(null); - const result = await triggersAndPollers.runPoll(workflow, node, pollFunctions); + const result = await runPollHelper(); expect(pollFn).toHaveBeenCalled(); expect(result).toBeNull(); }); + + it('should propagate errors from poll function', async () => { + nodeType.poll = pollFn; + pollFn.mockRejectedValue(new Error('Poll function failed')); + + await expect(runPollHelper()).rejects.toThrow('Poll function failed'); + expect(pollFn).toHaveBeenCalled(); + }); }); }); From 6c323e4e495833b14b6fe4ad40ca2194288f602c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 20 Dec 2024 16:47:39 +0100 Subject: [PATCH 03/66] ci: Shim WebCrypto on node.js 18 for core tests (#12335) --- packages/core/test/setup-mocks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/test/setup-mocks.ts b/packages/core/test/setup-mocks.ts index d2c9bc6e64..c36ff529c2 100644 --- a/packages/core/test/setup-mocks.ts +++ b/packages/core/test/setup-mocks.ts @@ -1 +1,6 @@ import 'reflect-metadata'; + +// WebCrypto Polyfill for older versions of Node.js 18 +if (!globalThis.crypto?.getRandomValues) { + globalThis.crypto = require('node:crypto').webcrypto; +} From d4116630a638195c7d87e01e2b5c151941636056 Mon Sep 17 00:00:00 2001 From: Ivan Atanasov Date: Fri, 20 Dec 2024 17:01:22 +0100 Subject: [PATCH 04/66] feat: (Execute Workflow Node): Inputs for Sub-workflows (#11830) (#11837) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Charlie Kolb Co-authored-by: Milorad FIlipović Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- cypress/e2e/48-subworkflow-inputs.cy.ts | 288 +++++++++ cypress/fixtures/Test_Subworkflow-Inputs.json | 70 +++ cypress/pages/ndv.ts | 5 + .../tools/ToolWorkflow/ToolWorkflow.node.ts | 589 +----------------- .../ToolWorkflow/v1/ToolWorkflowV1.node.ts | 241 +++++++ .../ToolWorkflow/v1/versionDescription.ts | 345 ++++++++++ .../ToolWorkflow/v2/ToolWorkflowV2.node.ts | 42 ++ .../ToolWorkflow/v2/ToolWorkflowV2.test.ts | 235 +++++++ .../ToolWorkflow/v2/utils/FromAIParser.ts | 284 +++++++++ .../v2/utils/WorkflowToolService.ts | 313 ++++++++++ .../ToolWorkflow/v2/versionDescription.ts | 151 +++++ .../dynamic-node-parameters.controller.ts | 16 + .../dynamic-node-parameters.service.ts | 87 ++- .../src/services/workflow-loader.service.ts | 19 + packages/core/src/CreateNodeAsTool.ts | 3 + packages/core/src/WorkflowExecute.ts | 2 +- .../core/src/node-execution-context/index.ts | 1 + .../local-load-options-context.ts | 70 +++ .../utils/validateValueAgainstSchema.ts | 12 +- .../workflow-node-context.ts | 36 ++ packages/editor-ui/src/api/nodeTypes.ts | 12 + .../src/components/ParameterInput.vue | 1 + .../src/components/ResourceMapper.test.ts | 23 +- .../ResourceMapper/MappingFields.vue | 43 +- .../ResourceMapper/ResourceMapper.vue | 163 ++++- .../src/composables/useDocumentVisibility.ts | 52 ++ packages/editor-ui/src/constants.workflows.ts | 5 +- .../src/plugins/i18n/locales/en.json | 11 +- .../editor-ui/src/stores/nodeTypes.store.ts | 11 + .../editor-ui/src/utils/nodeTypeUtils.test.ts | 75 +++ .../editor-ui/src/utils/nodeTypesUtils.ts | 36 ++ .../ExecuteWorkflow.node.json | 0 .../ExecuteWorkflow.node.test.ts | 113 ++++ .../ExecuteWorkflow.node.ts | 82 ++- .../{ => ExecuteWorkflow}/GenericFunctions.ts | 7 +- .../ExecuteWorkflowTrigger.node.json | 0 .../ExecuteWorkflowTrigger.node.test.ts | 53 ++ .../ExecuteWorkflowTrigger.node.ts | 225 +++++++ .../ExecuteWorkflowTrigger.node.ts | 55 -- .../test/ExecuteWorkflowTrigger.node.test.ts | 19 - packages/nodes-base/package.json | 5 +- packages/nodes-base/tsconfig.build.json | 3 +- packages/nodes-base/tsconfig.json | 8 +- .../nodes-base/types/generate-schema.d.ts | 27 + .../workflowInputsResourceMapping/.readme | 5 + .../GenericFunctions.ts | 167 +++++ .../constants.ts | 36 ++ packages/workflow/src/Interfaces.ts | 47 +- packages/workflow/src/NodeHelpers.ts | 20 +- packages/workflow/src/WorkflowDataProxy.ts | 7 +- packages/workflow/test/NodeHelpers.test.ts | 588 +++++++++++++++++ pnpm-lock.yaml | 3 + 52 files changed, 4023 insertions(+), 688 deletions(-) create mode 100644 cypress/e2e/48-subworkflow-inputs.cy.ts create mode 100644 cypress/fixtures/Test_Subworkflow-Inputs.json create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/ToolWorkflowV1.node.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/FromAIParser.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts create mode 100644 packages/cli/src/services/workflow-loader.service.ts create mode 100644 packages/core/src/node-execution-context/local-load-options-context.ts create mode 100644 packages/core/src/node-execution-context/workflow-node-context.ts create mode 100644 packages/editor-ui/src/composables/useDocumentVisibility.ts create mode 100644 packages/editor-ui/src/utils/nodeTypeUtils.test.ts rename packages/nodes-base/nodes/ExecuteWorkflow/{ => ExecuteWorkflow}/ExecuteWorkflow.node.json (100%) create mode 100644 packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts rename packages/nodes-base/nodes/ExecuteWorkflow/{ => ExecuteWorkflow}/ExecuteWorkflow.node.ts (82%) rename packages/nodes-base/nodes/ExecuteWorkflow/{ => ExecuteWorkflow}/GenericFunctions.ts (92%) rename packages/nodes-base/nodes/{ => ExecuteWorkflow}/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.json (100%) create mode 100644 packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts create mode 100644 packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts delete mode 100644 packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts delete mode 100644 packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts create mode 100644 packages/nodes-base/types/generate-schema.d.ts create mode 100644 packages/nodes-base/utils/workflowInputsResourceMapping/.readme create mode 100644 packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts create mode 100644 packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts diff --git a/cypress/e2e/48-subworkflow-inputs.cy.ts b/cypress/e2e/48-subworkflow-inputs.cy.ts new file mode 100644 index 0000000000..0e2755b9f0 --- /dev/null +++ b/cypress/e2e/48-subworkflow-inputs.cy.ts @@ -0,0 +1,288 @@ +import { clickGetBackToCanvas, getOutputTableHeaders } from '../composables/ndv'; +import { + clickZoomToFit, + navigateToNewWorkflowPage, + openNode, + pasteWorkflow, + saveWorkflowOnButtonClick, +} from '../composables/workflow'; +import SUB_WORKFLOW_INPUTS from '../fixtures/Test_Subworkflow-Inputs.json'; +import { NDV, WorkflowsPage, WorkflowPage } from '../pages'; +import { errorToast, successToast } from '../pages/notifications'; +import { getVisiblePopper } from '../utils'; + +const ndv = new NDV(); +const workflowsPage = new WorkflowsPage(); +const workflow = new WorkflowPage(); + +const DEFAULT_WORKFLOW_NAME = 'My workflow'; +const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1'; +const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2'; + +type FieldRow = readonly string[]; + +const exampleFields = [ + ['aNumber', 'Number'], + ['aString', 'String'], + ['aArray', 'Array'], + ['aObject', 'Object'], + ['aAny', 'Allow Any Type'], + // bool last since it's not an inputField so we'll skip it for some cases + ['aBool', 'Boolean'], +] as const; + +/** + * Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing + * + * @param items - 2D array of items to populate, i.e. [["myField1", "String"], [""] + * @param collectionName - name of the fixedCollection to populate + * @param offset - amount of 'parameter-input's before the fixedCollection under test + * @returns + */ +function populateFixedCollection( + items: readonly FieldRow[], + collectionName: string, + offset: number, +) { + if (items.length === 0) return; + const n = items[0].length; + for (const [i, params] of items.entries()) { + ndv.actions.addItemToFixedCollection(collectionName); + for (const [j, param] of params.entries()) { + ndv.getters + .fixedCollectionParameter(collectionName) + .getByTestId('parameter-input') + .eq(offset + i * n + j) + .type(`${param}{downArrow}{enter}`); + } + } +} + +function makeExample(type: TypeField) { + switch (type) { + case 'String': + return '"example"'; + case 'Number': + return '42'; + case 'Boolean': + return 'true'; + case 'Array': + return '["example", 123, null]'; + case 'Object': + return '{{}"example": [123]}'; + case 'Allow Any Type': + return 'null'; + } +} + +type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object'; +function populateFields(items: ReadonlyArray) { + populateFixedCollection(items, 'workflowInputs', 1); +} + +function navigateWorkflowSelectionDropdown(index: number, expectedText: string) { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist'); + getVisiblePopper() + .findChildByTestId('rlc-item') + .eq(index) + .find('span') + .should('have.text', expectedText) + .click(); +} + +function populateMapperFields(values: readonly string[], offset: number) { + for (const [i, value] of values.entries()) { + cy.getByTestId('parameter-input') + .eq(offset + i) + .type(value); + + // Click on a parent to dismiss the pop up hiding the field below. + cy.getByTestId('parameter-input') + .eq(offset + i) + .parent() + .parent() + .click('topLeft'); + } +} + +// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields +// It then navigates back to the parent and validates output +function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) { + ndv.actions.execute(); + + // + 1 to account for formatting-only column + getOutputTableHeaders().should('have.length', fields.length + 1); + for (const [i, name] of fields.entries()) { + getOutputTableHeaders().eq(i).should('have.text', name); + } + + clickGetBackToCanvas(); + saveWorkflowOnButtonClick(); + + cy.visit(workflowsPage.url); + + workflowsPage.getters.workflowCardContent(DEFAULT_WORKFLOW_NAME).click(); + + openNode('Execute Workflow'); + + // Note that outside of e2e tests this will be pre-selected correctly. + // Due to our workaround to remain in the same tab we need to select the correct tab manually + navigateWorkflowSelectionDropdown(offset, targetChild); + + // This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I + ndv.actions.execute(); + + getOutputTableHeaders().should('have.length', fields.length + 1); + for (const [i, name] of fields.entries()) { + getOutputTableHeaders().eq(i).should('have.text', name); + } + + // todo: verify the fields appear and show the correct types + + // todo: fill in the input fields (and mock previous node data in the json fixture to match) + + // todo: validate the actual output data +} + +function setWorkflowInputFieldValue(index: number, value: string) { + ndv.actions.addItemToFixedCollection('workflowInputs'); + ndv.actions.typeIntoFixedCollectionItem('workflowInputs', index, value); +} + +describe('Sub-workflow creation and typed usage', () => { + beforeEach(() => { + navigateToNewWorkflowPage(); + pasteWorkflow(SUB_WORKFLOW_INPUTS); + saveWorkflowOnButtonClick(); + clickZoomToFit(); + + openNode('Execute Workflow'); + + // Prevent sub-workflow from opening in new window + cy.window().then((win) => { + cy.stub(win, 'open').callsFake((url) => { + cy.visit(url); + }); + }); + navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow'); + // ************************** + // NAVIGATE TO CHILD WORKFLOW + // ************************** + + openNode('Workflow Input Trigger'); + }); + + it('works with type-checked values', () => { + populateFields(exampleFields); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_1, + 1, + exampleFields.map((f) => f[0]), + ); + + const values = [ + '-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it + ...exampleFields.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // }} are added automatically + ]; + + // this matches with the pinned data provided in the fixture + populateMapperFields(values, 2); + + ndv.actions.execute(); + + // todo: + // - validate output lines up + // - change input to need casts + // - run + // - confirm error + // - switch `attemptToConvertTypes` flag + // - confirm success and changed output + // - change input to be invalid despite cast + // - run + // - confirm error + // - switch type option flags + // - run + // - confirm success + // - turn off attempt to cast flag + // - confirm a value was not cast + }); + + it('works with Fields input source into JSON input source', () => { + ndv.getters.nodeOutputHint().should('exist'); + + populateFields(exampleFields); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_1, + 1, + exampleFields.map((f) => f[0]), + ); + + cy.window().then((win) => { + cy.stub(win, 'open').callsFake((url) => { + cy.visit(url); + }); + }); + navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow'); + + openNode('Workflow Input Trigger'); + + cy.getByTestId('parameter-input').eq(0).click(); + + // Todo: Check if there's a better way to interact with option dropdowns + // This PR would add this child testId + getVisiblePopper() + .getByTestId('parameter-input') + .eq(0) + .type('Using JSON Example{downArrow}{enter}'); + + const exampleJson = + '{{}' + exampleFields.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}'; + cy.getByTestId('parameter-input-jsonExample') + .find('.cm-line') + .eq(0) + .type(`{selectAll}{backspace}${exampleJson}{enter}`); + + // first one doesn't work for some reason, might need to wait for something? + ndv.actions.execute(); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_2, + 2, + exampleFields.map((f) => f[0]), + ); + + // test for either InputSource mode and options combinations: + // + we're showing the notice in the output panel + // + we start with no fields + // + Test Step works and we create the fields + // + create field of each type (string, number, boolean, object, array, any) + // + exit ndv + // + save + // + go back to parent workflow + // - verify fields appear [needs Ivan's PR] + // - link fields [needs Ivan's PR] + // + run parent + // - verify output with `null` defaults exists + // + }); + + it('should show node issue when no fields are defined in manual mode', () => { + ndv.getters.nodeExecuteButton().should('be.disabled'); + ndv.actions.close(); + // Executing the workflow should show an error toast + workflow.actions.executeWorkflow(); + errorToast().should('contain', 'The workflow has issues'); + openNode('Workflow Input Trigger'); + // Add a field to the workflowInputs fixedCollection + setWorkflowInputFieldValue(0, 'test'); + // Executing the workflow should not show error now + ndv.actions.close(); + workflow.actions.executeWorkflow(); + successToast().should('contain', 'Workflow executed successfully'); + }); +}); diff --git a/cypress/fixtures/Test_Subworkflow-Inputs.json b/cypress/fixtures/Test_Subworkflow-Inputs.json new file mode 100644 index 0000000000..aeb4d601fd --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow-Inputs.json @@ -0,0 +1,70 @@ +{ + "meta": { + "instanceId": "4d0676b62208d810ef035130bbfc9fd3afdc78d963ea8ccb9514dc89066efc94" + }, + "nodes": [ + { + "parameters": {}, + "id": "bb7f8bb3-840a-464c-a7de-d3a80538c2be", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0] + }, + { + "parameters": { + "workflowId": {}, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "ignoreTypeMismatchErrors": false, + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [500, 240], + "id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453", + "name": "Execute Workflow" + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Execute Workflow", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "When clicking ‘Test workflow’": [ + { + "aaString": "A String", + "aaNumber": 1, + "aaArray": [1, true, "3"], + "aaObject": { + "aKey": -1 + }, + "aaAny": {} + }, + { + "aaString": "Another String", + "aaNumber": 2, + "aaArray": [], + "aaObject": { + "aDifferentKey": -1 + }, + "aaAny": [] + } + ] + } +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 4550da8e2a..1926ef0ad1 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -320,6 +320,11 @@ export class NDV extends BasePage { addItemToFixedCollection: (paramName: string) => { this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click(); }, + typeIntoFixedCollectionItem: (fixedCollectionName: string, index: number, content: string) => { + this.getters.fixedCollectionParameter(fixedCollectionName).within(() => { + cy.getByTestId('parameter-input').eq(index).type(content); + }); + }, dragMainPanelToLeft: () => { cy.drag('[data-test-id=panel-drag-button]', [-1000, 0], { moveTwice: true }); }, diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index 227481b65c..de7abf6a8b 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -1,567 +1,42 @@ -import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; -import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools'; -import type { JSONSchema7 } from 'json-schema'; -import get from 'lodash/get'; -import isObject from 'lodash/isObject'; -import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; -import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; -import type { - IExecuteWorkflowInfo, - INodeExecutionData, - INodeType, - INodeTypeDescription, - IWorkflowBase, - ISupplyDataFunctions, - SupplyData, - ExecutionError, - ExecuteWorkflowData, - IDataObject, - INodeParameterResourceLocator, - ITaskMetadata, -} from 'n8n-workflow'; -import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow'; +import type { IVersionedNodeType, INodeTypeBaseDescription } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import { jsonSchemaExampleField, schemaTypeField, inputSchemaField } from '@utils/descriptions'; -import { convertJsonSchemaToZod, generateSchema } from '@utils/schemaParsing'; -import { getConnectionHintNoticeField } from '@utils/sharedFields'; +import { ToolWorkflowV1 } from './v1/ToolWorkflowV1.node'; +import { ToolWorkflowV2 } from './v2/ToolWorkflowV2.node'; -import type { DynamicZodObject } from '../../../types/zod.types'; - -export class ToolWorkflow implements INodeType { - description: INodeTypeDescription = { - displayName: 'Call n8n Workflow Tool', - name: 'toolWorkflow', - icon: 'fa:network-wired', - iconColor: 'black', - group: ['transform'], - version: [1, 1.1, 1.2, 1.3], - description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', - defaults: { - name: 'Call n8n Workflow Tool', - }, - codex: { - categories: ['AI'], - subcategories: { - AI: ['Tools'], - Tools: ['Recommended Tools'], - }, - resources: { - primaryDocumentation: [ - { - url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolworkflow/', - }, - ], - }, - }, - // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node - inputs: [], - // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong - outputs: [NodeConnectionType.AiTool], - outputNames: ['Tool'], - properties: [ - getConnectionHintNoticeField([NodeConnectionType.AiAgent]), - { - displayName: - 'See an example of a workflow to suggest meeting slots using AI here.', - name: 'noticeTemplateExample', - type: 'notice', - default: '', - }, - { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - placeholder: 'My_Color_Tool', - displayOptions: { - show: { - '@version': [1], - }, +export class ToolWorkflow extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Call n8n Sub-Workflow Tool', + name: 'toolWorkflow', + icon: 'fa:network-wired', + group: ['transform'], + description: + 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', + codex: { + categories: ['AI'], + subcategories: { + AI: ['Tools'], + Tools: ['Recommended Tools'], }, - }, - { - 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', - name: 'description', - type: 'string', - default: '', - placeholder: - 'Call this tool to get a random color. The input should be a string with comma separted names of colors to exclude.', - typeOptions: { - rows: 3, - }, - }, - - { - displayName: - 'This tool will call the workflow you define below, and look in the last node for the response. The workflow needs to start with an Execute Workflow trigger', - name: 'executeNotice', - type: 'notice', - default: '', - }, - - { - displayName: 'Source', - name: 'source', - type: 'options', - options: [ - { - name: 'Database', - value: 'database', - description: 'Load the workflow from the database by ID', - }, - { - name: 'Define Below', - value: 'parameter', - description: 'Pass the JSON code of a workflow', - }, - ], - default: 'database', - description: 'Where to get the workflow to execute from', - }, - - // ---------------------------------- - // source:database - // ---------------------------------- - { - displayName: 'Workflow ID', - name: 'workflowId', - type: 'string', - displayOptions: { - show: { - source: ['database'], - '@version': [{ _cnd: { lte: 1.1 } }], - }, - }, - default: '', - required: true, - description: 'The workflow to execute', - hint: 'Can be found in the URL of the workflow', - }, - - { - displayName: 'Workflow', - name: 'workflowId', - type: 'workflowSelector', - displayOptions: { - show: { - source: ['database'], - '@version': [{ _cnd: { gte: 1.2 } }], - }, - }, - default: '', - required: true, - }, - - // ---------------------------------- - // source:parameter - // ---------------------------------- - { - displayName: 'Workflow JSON', - name: 'workflowJson', - type: 'json', - typeOptions: { - rows: 10, - }, - displayOptions: { - show: { - source: ['parameter'], - }, - }, - default: '\n\n\n\n\n\n\n\n\n', - required: true, - description: 'The workflow JSON code to execute', - }, - // ---------------------------------- - // For all - // ---------------------------------- - { - displayName: 'Field to Return', - name: 'responsePropertyName', - type: 'string', - default: 'response', - required: true, - hint: 'The field in the last-executed node of the workflow that contains the response', - description: - 'Where to find the data that this tool should return. n8n will look in the output of the last-executed node of the workflow for a field with this name, and return its value.', - displayOptions: { - show: { - '@version': [{ _cnd: { lt: 1.3 } }], - }, - }, - }, - { - displayName: 'Extra Workflow Inputs', - name: 'fields', - placeholder: 'Add Value', - type: 'fixedCollection', - description: - "These will be output by the 'execute workflow' trigger of the workflow being called", - typeOptions: { - multipleValues: true, - sortable: true, - }, - default: {}, - options: [ - { - name: 'values', - displayName: 'Values', - values: [ - { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - placeholder: 'e.g. fieldName', - description: - 'Name of the field to set the value of. Supports dot-notation. Example: data.person[0].name.', - requiresDataPath: 'single', - }, - { - displayName: 'Type', - name: 'type', - type: 'options', - description: 'The field value type', - // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items - options: [ - { - name: 'String', - value: 'stringValue', - }, - { - name: 'Number', - value: 'numberValue', - }, - { - name: 'Boolean', - value: 'booleanValue', - }, - { - name: 'Array', - value: 'arrayValue', - }, - { - name: 'Object', - value: 'objectValue', - }, - ], - default: 'stringValue', - }, - { - displayName: 'Value', - name: 'stringValue', - type: 'string', - default: '', - displayOptions: { - show: { - type: ['stringValue'], - }, - }, - validateType: 'string', - ignoreValidationDuringExecution: true, - }, - { - displayName: 'Value', - name: 'numberValue', - type: 'string', - default: '', - displayOptions: { - show: { - type: ['numberValue'], - }, - }, - validateType: 'number', - ignoreValidationDuringExecution: true, - }, - { - displayName: 'Value', - name: 'booleanValue', - type: 'options', - default: 'true', - options: [ - { - name: 'True', - value: 'true', - }, - { - name: 'False', - value: 'false', - }, - ], - displayOptions: { - show: { - type: ['booleanValue'], - }, - }, - validateType: 'boolean', - ignoreValidationDuringExecution: true, - }, - { - displayName: 'Value', - name: 'arrayValue', - type: 'string', - default: '', - placeholder: 'e.g. [ arrayItem1, arrayItem2, arrayItem3 ]', - displayOptions: { - show: { - type: ['arrayValue'], - }, - }, - validateType: 'array', - ignoreValidationDuringExecution: true, - }, - { - displayName: 'Value', - name: 'objectValue', - type: 'json', - default: '={}', - typeOptions: { - rows: 2, - }, - displayOptions: { - show: { - type: ['objectValue'], - }, - }, - validateType: 'object', - ignoreValidationDuringExecution: true, - }, - ], - }, - ], - }, - // ---------------------------------- - // Output Parsing - // ---------------------------------- - { - displayName: 'Specify Input Schema', - name: 'specifyInputSchema', - type: 'boolean', - description: - 'Whether to specify the schema for the function. This would require the LLM to provide the input in the correct format and would validate it against the schema.', - noDataExpression: true, - default: false, - }, - { ...schemaTypeField, displayOptions: { show: { specifyInputSchema: [true] } } }, - jsonSchemaExampleField, - inputSchemaField, - ], - }; - - async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { - const workflowProxy = this.getWorkflowDataProxy(0); - - const name = this.getNodeParameter('name', itemIndex) as string; - const description = this.getNodeParameter('description', itemIndex) as string; - - let subExecutionId: string | undefined; - let subWorkflowId: string | undefined; - - const useSchema = this.getNodeParameter('specifyInputSchema', itemIndex) as boolean; - let tool: DynamicTool | DynamicStructuredTool | undefined = undefined; - - const runFunction = async ( - query: string | IDataObject, - runManager?: CallbackManagerForToolRun, - ): Promise => { - const source = this.getNodeParameter('source', itemIndex) as string; - const workflowInfo: IExecuteWorkflowInfo = {}; - if (source === 'database') { - // Read workflow from database - const nodeVersion = this.getNode().typeVersion; - if (nodeVersion <= 1.1) { - workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string; - } else { - const { value } = this.getNodeParameter( - 'workflowId', - itemIndex, - {}, - ) as INodeParameterResourceLocator; - workflowInfo.id = value as string; - } - - subWorkflowId = workflowInfo.id; - } else if (source === 'parameter') { - // Read workflow from parameter - const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string; - try { - workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase; - - // subworkflow is same as parent workflow - subWorkflowId = workflowProxy.$workflow.id; - } catch (error) { - throw new NodeOperationError( - this.getNode(), - `The provided workflow is not valid JSON: "${(error as Error).message}"`, + resources: { + primaryDocumentation: [ { - itemIndex, + url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolworkflow/', }, - ); - } - } - - const rawData: IDataObject = { query }; - - const workflowFieldsJson = this.getNodeParameter('fields.values', itemIndex, [], { - rawExpressions: true, - }) as SetField[]; - - // Copied from Set Node v2 - for (const entry of workflowFieldsJson) { - if (entry.type === 'objectValue' && (entry.objectValue as string).startsWith('=')) { - rawData[entry.name] = (entry.objectValue as string).replace(/^=+/, ''); - } - } - - const options: SetNodeOptions = { - include: 'all', - }; - - const newItem = await manual.execute.call( - this, - { json: { query } }, - itemIndex, - options, - rawData, - this.getNode(), - ); - - const items = [newItem] as INodeExecutionData[]; - - let receivedData: ExecuteWorkflowData; - try { - receivedData = await this.executeWorkflow(workflowInfo, items, runManager?.getChild(), { - parentExecution: { - executionId: workflowProxy.$execution.id, - workflowId: workflowProxy.$workflow.id, - }, - }); - subExecutionId = receivedData.executionId; - } catch (error) { - // Make sure a valid error gets returned that can by json-serialized else it will - // not show up in the frontend - throw new NodeOperationError(this.getNode(), error as Error); - } - - const response: string | undefined = get(receivedData, 'data[0][0].json') as - | string - | undefined; - if (response === undefined) { - throw new NodeOperationError( - this.getNode(), - 'There was an error: "The workflow did not return a response"', - ); - } - - return response; + ], + }, + }, + defaultVersion: 2, }; - const toolHandler = async ( - query: string | IDataObject, - runManager?: CallbackManagerForToolRun, - ): Promise => { - const { index } = this.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]); - - let response: string = ''; - let executionError: ExecutionError | undefined; - try { - response = await runFunction(query, runManager); - } catch (error) { - // TODO: Do some more testing. Issues here should actually fail the workflow - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - executionError = error; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - response = `There was an error: "${error.message}"`; - } - - if (typeof response === 'number') { - response = (response as number).toString(); - } - - if (isObject(response)) { - response = JSON.stringify(response, null, 2); - } - - if (typeof response !== 'string') { - // TODO: Do some more testing. Issues here should actually fail the workflow - executionError = new NodeOperationError(this.getNode(), 'Wrong output type returned', { - description: `The response property should be a string, but it is an ${typeof response}`, - }); - response = `There was an error: "${executionError.message}"`; - } - - let metadata: ITaskMetadata | undefined; - if (subExecutionId && subWorkflowId) { - metadata = { - subExecution: { - executionId: subExecutionId, - workflowId: subWorkflowId, - }, - }; - } - - if (executionError) { - void this.addOutputData(NodeConnectionType.AiTool, index, executionError, metadata); - } else { - // Output always needs to be an object - // so we try to parse the response as JSON and if it fails we just return the string wrapped in an object - const json = jsonParse(response, { fallbackValue: { response } }); - void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json }]], metadata); - } - return response; - }; - - const functionBase = { - name, - description, - func: toolHandler, - }; - - if (useSchema) { - try { - // We initialize these even though one of them will always be empty - // it makes it easier to navigate the ternary operator - const jsonExample = this.getNodeParameter('jsonSchemaExample', itemIndex, '') as string; - const inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as string; - - const schemaType = this.getNodeParameter('schemaType', itemIndex) as 'fromJson' | 'manual'; - const jsonSchema = - schemaType === 'fromJson' - ? generateSchema(jsonExample) - : jsonParse(inputSchema); - - const zodSchema = convertJsonSchemaToZod(jsonSchema); - - tool = new DynamicStructuredTool({ - schema: zodSchema, - ...functionBase, - }); - } catch (error) { - throw new NodeOperationError( - this.getNode(), - 'Error during parsing of JSON Schema. \n ' + error, - ); - } - } else { - tool = new DynamicTool(functionBase); - } - - return { - response: tool, + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new ToolWorkflowV1(baseDescription), + 1.1: new ToolWorkflowV1(baseDescription), + 1.2: new ToolWorkflowV1(baseDescription), + 1.3: new ToolWorkflowV1(baseDescription), + 2: new ToolWorkflowV2(baseDescription), }; + super(nodeVersions, baseDescription); } } diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/ToolWorkflowV1.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/ToolWorkflowV1.node.ts new file mode 100644 index 0000000000..4c33c86b4e --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/ToolWorkflowV1.node.ts @@ -0,0 +1,241 @@ +import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; +import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools'; +import type { JSONSchema7 } from 'json-schema'; +import get from 'lodash/get'; +import isObject from 'lodash/isObject'; +import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; +import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; +import type { + IExecuteWorkflowInfo, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IWorkflowBase, + ISupplyDataFunctions, + SupplyData, + ExecutionError, + ExecuteWorkflowData, + IDataObject, + INodeParameterResourceLocator, + ITaskMetadata, + INodeTypeBaseDescription, +} from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow'; + +import { versionDescription } from './versionDescription'; +import type { DynamicZodObject } from '../../../../types/zod.types'; +import { convertJsonSchemaToZod, generateSchema } from '../../../../utils/schemaParsing'; + +export class ToolWorkflowV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { + const workflowProxy = this.getWorkflowDataProxy(0); + + const name = this.getNodeParameter('name', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex) as string; + + let subExecutionId: string | undefined; + let subWorkflowId: string | undefined; + + const useSchema = this.getNodeParameter('specifyInputSchema', itemIndex) as boolean; + let tool: DynamicTool | DynamicStructuredTool | undefined = undefined; + + const runFunction = async ( + query: string | IDataObject, + runManager?: CallbackManagerForToolRun, + ): Promise => { + const source = this.getNodeParameter('source', itemIndex) as string; + const workflowInfo: IExecuteWorkflowInfo = {}; + if (source === 'database') { + // Read workflow from database + const nodeVersion = this.getNode().typeVersion; + if (nodeVersion <= 1.1) { + workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string; + } else { + const { value } = this.getNodeParameter( + 'workflowId', + itemIndex, + {}, + ) as INodeParameterResourceLocator; + workflowInfo.id = value as string; + } + + subWorkflowId = workflowInfo.id; + } else if (source === 'parameter') { + // Read workflow from parameter + const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string; + try { + workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase; + + // subworkflow is same as parent workflow + subWorkflowId = workflowProxy.$workflow.id; + } catch (error) { + throw new NodeOperationError( + this.getNode(), + `The provided workflow is not valid JSON: "${(error as Error).message}"`, + { + itemIndex, + }, + ); + } + } + + const rawData: IDataObject = { query }; + + const workflowFieldsJson = this.getNodeParameter('fields.values', itemIndex, [], { + rawExpressions: true, + }) as SetField[]; + + // Copied from Set Node v2 + for (const entry of workflowFieldsJson) { + if (entry.type === 'objectValue' && (entry.objectValue as string).startsWith('=')) { + rawData[entry.name] = (entry.objectValue as string).replace(/^=+/, ''); + } + } + + const options: SetNodeOptions = { + include: 'all', + }; + + const newItem = await manual.execute.call( + this, + { json: { query } }, + itemIndex, + options, + rawData, + this.getNode(), + ); + + const items = [newItem] as INodeExecutionData[]; + + let receivedData: ExecuteWorkflowData; + try { + receivedData = await this.executeWorkflow(workflowInfo, items, runManager?.getChild(), { + parentExecution: { + executionId: workflowProxy.$execution.id, + workflowId: workflowProxy.$workflow.id, + }, + }); + subExecutionId = receivedData.executionId; + } catch (error) { + // Make sure a valid error gets returned that can by json-serialized else it will + // not show up in the frontend + throw new NodeOperationError(this.getNode(), error as Error); + } + + const response: string | undefined = get(receivedData, 'data[0][0].json') as + | string + | undefined; + if (response === undefined) { + throw new NodeOperationError( + this.getNode(), + 'There was an error: "The workflow did not return a response"', + ); + } + + return response; + }; + + const toolHandler = async ( + query: string | IDataObject, + runManager?: CallbackManagerForToolRun, + ): Promise => { + const { index } = this.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]); + + let response: string = ''; + let executionError: ExecutionError | undefined; + try { + response = await runFunction(query, runManager); + } catch (error) { + // TODO: Do some more testing. Issues here should actually fail the workflow + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + executionError = error; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + response = `There was an error: "${error.message}"`; + } + + if (typeof response === 'number') { + response = (response as number).toString(); + } + + if (isObject(response)) { + response = JSON.stringify(response, null, 2); + } + + if (typeof response !== 'string') { + // TODO: Do some more testing. Issues here should actually fail the workflow + executionError = new NodeOperationError(this.getNode(), 'Wrong output type returned', { + description: `The response property should be a string, but it is an ${typeof response}`, + }); + response = `There was an error: "${executionError.message}"`; + } + + let metadata: ITaskMetadata | undefined; + if (subExecutionId && subWorkflowId) { + metadata = { + subExecution: { + executionId: subExecutionId, + workflowId: subWorkflowId, + }, + }; + } + + if (executionError) { + void this.addOutputData(NodeConnectionType.AiTool, index, executionError, metadata); + } else { + // Output always needs to be an object + // so we try to parse the response as JSON and if it fails we just return the string wrapped in an object + const json = jsonParse(response, { fallbackValue: { response } }); + void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json }]], metadata); + } + return response; + }; + + const functionBase = { + name, + description, + func: toolHandler, + }; + + if (useSchema) { + try { + // We initialize these even though one of them will always be empty + // it makes it easier to navigate the ternary operator + const jsonExample = this.getNodeParameter('jsonSchemaExample', itemIndex, '') as string; + const inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as string; + + const schemaType = this.getNodeParameter('schemaType', itemIndex) as 'fromJson' | 'manual'; + const jsonSchema = + schemaType === 'fromJson' + ? generateSchema(jsonExample) + : jsonParse(inputSchema); + + const zodSchema = convertJsonSchemaToZod(jsonSchema); + + tool = new DynamicStructuredTool({ + schema: zodSchema, + ...functionBase, + }); + } catch (error) { + throw new NodeOperationError( + this.getNode(), + 'Error during parsing of JSON Schema. \n ' + error, + ); + } + } else { + tool = new DynamicTool(functionBase); + } + + return { + response: tool, + }; + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts new file mode 100644 index 0000000000..da7a0e9815 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts @@ -0,0 +1,345 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; + +import { + inputSchemaField, + jsonSchemaExampleField, + schemaTypeField, +} from '../../../../utils/descriptions'; +import { getConnectionHintNoticeField } from '../../../../utils/sharedFields'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Call n8n Workflow Tool', + name: 'toolWorkflow', + icon: 'fa:network-wired', + iconColor: 'black', + group: ['transform'], + version: [1, 1.1, 1.2, 1.3], + description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', + defaults: { + name: 'Call n8n Workflow Tool', + }, + codex: { + categories: ['AI'], + subcategories: { + AI: ['Tools'], + Tools: ['Recommended Tools'], + }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolworkflow/', + }, + ], + }, + }, + // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node + inputs: [], + // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong + outputs: [NodeConnectionType.AiTool], + outputNames: ['Tool'], + properties: [ + getConnectionHintNoticeField([NodeConnectionType.AiAgent]), + { + displayName: + 'See an example of a workflow to suggest meeting slots using AI here.', + name: 'noticeTemplateExample', + type: 'notice', + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + 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', + name: 'description', + type: 'string', + default: '', + placeholder: + 'Call this tool to get a random color. The input should be a string with comma separted names of colors to exclude.', + typeOptions: { + rows: 3, + }, + }, + + { + displayName: + 'This tool will call the workflow you define below, and look in the last node for the response. The workflow needs to start with an Execute Workflow trigger', + name: 'executeNotice', + type: 'notice', + default: '', + }, + + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + description: 'Load the workflow from the database by ID', + }, + { + name: 'Define Below', + value: 'parameter', + description: 'Pass the JSON code of a workflow', + }, + ], + default: 'database', + description: 'Where to get the workflow to execute from', + }, + + // ---------------------------------- + // source:database + // ---------------------------------- + { + displayName: 'Workflow ID', + name: 'workflowId', + type: 'string', + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { lte: 1.1 } }], + }, + }, + default: '', + required: true, + description: 'The workflow to execute', + hint: 'Can be found in the URL of the workflow', + }, + + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { gte: 1.2 } }], + }, + }, + default: '', + required: true, + }, + + // ---------------------------------- + // source:parameter + // ---------------------------------- + { + displayName: 'Workflow JSON', + name: 'workflowJson', + type: 'json', + typeOptions: { + rows: 10, + }, + displayOptions: { + show: { + source: ['parameter'], + }, + }, + default: '\n\n\n\n\n\n\n\n\n', + required: true, + description: 'The workflow JSON code to execute', + }, + // ---------------------------------- + // For all + // ---------------------------------- + { + displayName: 'Field to Return', + name: 'responsePropertyName', + type: 'string', + default: 'response', + required: true, + hint: 'The field in the last-executed node of the workflow that contains the response', + description: + 'Where to find the data that this tool should return. n8n will look in the output of the last-executed node of the workflow for a field with this name, and return its value.', + displayOptions: { + show: { + '@version': [{ _cnd: { lt: 1.3 } }], + }, + }, + }, + { + displayName: 'Extra Workflow Inputs', + name: 'fields', + placeholder: 'Add Value', + type: 'fixedCollection', + description: + "These will be output by the 'execute workflow' trigger of the workflow being called", + typeOptions: { + multipleValues: true, + sortable: true, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: + 'Name of the field to set the value of. Supports dot-notation. Example: data.person[0].name.', + requiresDataPath: 'single', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items + options: [ + { + name: 'String', + value: 'stringValue', + }, + { + name: 'Number', + value: 'numberValue', + }, + { + name: 'Boolean', + value: 'booleanValue', + }, + { + name: 'Array', + value: 'arrayValue', + }, + { + name: 'Object', + value: 'objectValue', + }, + ], + default: 'stringValue', + }, + { + displayName: 'Value', + name: 'stringValue', + type: 'string', + default: '', + displayOptions: { + show: { + type: ['stringValue'], + }, + }, + validateType: 'string', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'numberValue', + type: 'string', + default: '', + displayOptions: { + show: { + type: ['numberValue'], + }, + }, + validateType: 'number', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'booleanValue', + type: 'options', + default: 'true', + options: [ + { + name: 'True', + value: 'true', + }, + { + name: 'False', + value: 'false', + }, + ], + displayOptions: { + show: { + type: ['booleanValue'], + }, + }, + validateType: 'boolean', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'arrayValue', + type: 'string', + default: '', + placeholder: 'e.g. [ arrayItem1, arrayItem2, arrayItem3 ]', + displayOptions: { + show: { + type: ['arrayValue'], + }, + }, + validateType: 'array', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'objectValue', + type: 'json', + default: '={}', + typeOptions: { + rows: 2, + }, + displayOptions: { + show: { + type: ['objectValue'], + }, + }, + validateType: 'object', + ignoreValidationDuringExecution: true, + }, + ], + }, + ], + }, + // ---------------------------------- + // Output Parsing + // ---------------------------------- + { + displayName: 'Specify Input Schema', + name: 'specifyInputSchema', + type: 'boolean', + description: + 'Whether to specify the schema for the function. This would require the LLM to provide the input in the correct format and would validate it against the schema.', + noDataExpression: true, + default: false, + }, + { ...schemaTypeField, displayOptions: { show: { specifyInputSchema: [true] } } }, + jsonSchemaExampleField, + inputSchemaField, + ], +}; diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts new file mode 100644 index 0000000000..22ca31e4da --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts @@ -0,0 +1,42 @@ +import { loadWorkflowInputMappings } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; +import type { + INodeTypeBaseDescription, + ISupplyDataFunctions, + SupplyData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { WorkflowToolService } from './utils/WorkflowToolService'; +import { versionDescription } from './versionDescription'; + +export class ToolWorkflowV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + localResourceMapping: { + loadWorkflowInputMappings, + }, + }; + + async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { + const workflowToolService = new WorkflowToolService(this); + const name = this.getNodeParameter('name', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex) as string; + + const tool = await workflowToolService.createTool({ + name, + description, + itemIndex, + }); + + return { response: tool }; + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts new file mode 100644 index 0000000000..73aa24c6b7 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts @@ -0,0 +1,235 @@ +/* eslint-disable @typescript-eslint/dot-notation */ // Disabled to allow access to private methods +import { DynamicTool } from '@langchain/core/tools'; +import { NodeOperationError } from 'n8n-workflow'; +import type { + ISupplyDataFunctions, + INodeExecutionData, + IWorkflowDataProxyData, + ExecuteWorkflowData, + INode, +} from 'n8n-workflow'; + +import { WorkflowToolService } from './utils/WorkflowToolService'; + +// Mock ISupplyDataFunctions interface +function createMockContext(overrides?: Partial): ISupplyDataFunctions { + return { + getNodeParameter: jest.fn(), + getWorkflowDataProxy: jest.fn(), + getNode: jest.fn(), + executeWorkflow: jest.fn(), + addInputData: jest.fn(), + addOutputData: jest.fn(), + getCredentials: jest.fn(), + getCredentialsProperties: jest.fn(), + getInputData: jest.fn(), + getMode: jest.fn(), + getRestApiUrl: jest.fn(), + getTimezone: jest.fn(), + getWorkflow: jest.fn(), + getWorkflowStaticData: jest.fn(), + logger: { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + }, + ...overrides, + } as ISupplyDataFunctions; +} + +describe('WorkflowTool::WorkflowToolService', () => { + let context: ISupplyDataFunctions; + let service: WorkflowToolService; + + beforeEach(() => { + // Prepare essential mocks + context = createMockContext(); + jest.spyOn(context, 'getNode').mockReturnValue({ + parameters: { workflowInputs: { schema: [] } }, + } as unknown as INode); + service = new WorkflowToolService(context); + }); + + describe('createTool', () => { + it('should create a basic dynamic tool when schema is not used', async () => { + const toolParams = { + name: 'TestTool', + description: 'Test Description', + itemIndex: 0, + }; + + const result = await service.createTool(toolParams); + + expect(result).toBeInstanceOf(DynamicTool); + expect(result).toHaveProperty('name', 'TestTool'); + expect(result).toHaveProperty('description', 'Test Description'); + }); + + it('should create a tool that can handle successful execution', async () => { + const toolParams = { + name: 'TestTool', + description: 'Test Description', + itemIndex: 0, + }; + + const TEST_RESPONSE = { msg: 'test response' }; + + const mockExecuteWorkflowResponse: ExecuteWorkflowData = { + data: [[{ json: TEST_RESPONSE }]], + executionId: 'test-execution', + }; + + jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockExecuteWorkflowResponse); + jest.spyOn(context, 'addInputData').mockReturnValue({ index: 0 }); + jest.spyOn(context, 'getNodeParameter').mockReturnValue('database'); + jest.spyOn(context, 'getWorkflowDataProxy').mockReturnValue({ + $execution: { id: 'exec-id' }, + $workflow: { id: 'workflow-id' }, + } as unknown as IWorkflowDataProxyData); + + const tool = await service.createTool(toolParams); + const result = await tool.func('test query'); + + expect(result).toBe(JSON.stringify(TEST_RESPONSE, null, 2)); + expect(context.addOutputData).toHaveBeenCalled(); + }); + + it('should handle errors during tool execution', async () => { + const toolParams = { + name: 'TestTool', + description: 'Test Description', + itemIndex: 0, + }; + + jest + .spyOn(context, 'executeWorkflow') + .mockRejectedValueOnce(new Error('Workflow execution failed')); + jest.spyOn(context, 'addInputData').mockReturnValue({ index: 0 }); + jest.spyOn(context, 'getNodeParameter').mockReturnValue('database'); + + const tool = await service.createTool(toolParams); + const result = await tool.func('test query'); + + expect(result).toContain('There was an error'); + expect(context.addOutputData).toHaveBeenCalled(); + }); + }); + + describe('handleToolResponse', () => { + it('should handle number response', () => { + const result = service['handleToolResponse'](42); + + expect(result).toBe('42'); + }); + + it('should handle object response', () => { + const obj = { test: 'value' }; + + const result = service['handleToolResponse'](obj); + + expect(result).toBe(JSON.stringify(obj, null, 2)); + }); + + it('should handle string response', () => { + const result = service['handleToolResponse']('test response'); + + expect(result).toBe('test response'); + }); + + it('should throw error for invalid response type', () => { + expect(() => service['handleToolResponse'](undefined)).toThrow(NodeOperationError); + }); + }); + + describe('executeSubWorkflow', () => { + it('should successfully execute workflow and return response', async () => { + const workflowInfo = { id: 'test-workflow' }; + const items: INodeExecutionData[] = []; + const workflowProxyMock = { + $execution: { id: 'exec-id' }, + $workflow: { id: 'workflow-id' }, + } as unknown as IWorkflowDataProxyData; + + const TEST_RESPONSE = { msg: 'test response' }; + + const mockResponse: ExecuteWorkflowData = { + data: [[{ json: TEST_RESPONSE }]], + executionId: 'test-execution', + }; + + jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse); + + const result = await service['executeSubWorkflow'](workflowInfo, items, workflowProxyMock); + + expect(result.response).toBe(TEST_RESPONSE); + expect(result.subExecutionId).toBe('test-execution'); + }); + + it('should throw error when workflow execution fails', async () => { + jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed')); + + await expect(service['executeSubWorkflow']({}, [], {} as never)).rejects.toThrow( + NodeOperationError, + ); + }); + + it('should throw error when workflow returns no response', async () => { + const mockResponse: ExecuteWorkflowData = { + data: [], + executionId: 'test-execution', + }; + + jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse); + + await expect(service['executeSubWorkflow']({}, [], {} as never)).rejects.toThrow(); + }); + }); + + describe('getSubWorkflowInfo', () => { + it('should handle database source correctly', async () => { + const source = 'database'; + const itemIndex = 0; + const workflowProxyMock = { + $workflow: { id: 'proxy-id' }, + } as unknown as IWorkflowDataProxyData; + + jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce({ value: 'workflow-id' }); + + const result = await service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock); + + expect(result.workflowInfo).toHaveProperty('id', 'workflow-id'); + expect(result.subWorkflowId).toBe('workflow-id'); + }); + + it('should handle parameter source correctly', async () => { + const source = 'parameter'; + const itemIndex = 0; + const workflowProxyMock = { + $workflow: { id: 'proxy-id' }, + } as unknown as IWorkflowDataProxyData; + const mockWorkflow = { id: 'test-workflow' }; + + jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce(JSON.stringify(mockWorkflow)); + + const result = await service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock); + + expect(result.workflowInfo.code).toEqual(mockWorkflow); + expect(result.subWorkflowId).toBe('proxy-id'); + }); + + it('should throw error for invalid JSON in parameter source', async () => { + const source = 'parameter'; + const itemIndex = 0; + const workflowProxyMock = { + $workflow: { id: 'proxy-id' }, + } as unknown as IWorkflowDataProxyData; + + jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce('invalid json'); + + await expect( + service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock), + ).rejects.toThrow(NodeOperationError); + }); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/FromAIParser.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/FromAIParser.ts new file mode 100644 index 0000000000..4b9b6ed58e --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/FromAIParser.ts @@ -0,0 +1,284 @@ +import type { ISupplyDataFunctions } from 'n8n-workflow'; +import { jsonParse, NodeOperationError } from 'n8n-workflow'; +import { z } from 'zod'; + +type AllowedTypes = 'string' | 'number' | 'boolean' | 'json'; +export interface FromAIArgument { + key: string; + description?: string; + type?: AllowedTypes; + defaultValue?: string | number | boolean | Record; +} + +// TODO: We copied this class from the core package, once the new nodes context work is merged, this should be available in root node context and this file can be removed. +// Please apply any changes to both files + +/** + * AIParametersParser + * + * This class encapsulates the logic for parsing node parameters, extracting $fromAI calls, + * generating Zod schemas, and creating LangChain tools. + */ +export class AIParametersParser { + private ctx: ISupplyDataFunctions; + + /** + * Constructs an instance of AIParametersParser. + * @param ctx The execution context. + */ + constructor(ctx: ISupplyDataFunctions) { + this.ctx = ctx; + } + + /** + * Generates a Zod schema based on the provided FromAIArgument placeholder. + * @param placeholder The FromAIArgument object containing key, type, description, and defaultValue. + * @returns A Zod schema corresponding to the placeholder's type and constraints. + */ + generateZodSchema(placeholder: FromAIArgument): z.ZodTypeAny { + let schema: z.ZodTypeAny; + + switch (placeholder.type?.toLowerCase()) { + case 'string': + schema = z.string(); + break; + case 'number': + schema = z.number(); + break; + case 'boolean': + schema = z.boolean(); + break; + case 'json': + schema = z.record(z.any()); + break; + default: + schema = z.string(); + } + + if (placeholder.description) { + schema = schema.describe(`${schema.description ?? ''} ${placeholder.description}`.trim()); + } + + if (placeholder.defaultValue !== undefined) { + schema = schema.default(placeholder.defaultValue); + } + + return schema; + } + + /** + * Recursively traverses the nodeParameters object to find all $fromAI calls. + * @param payload The current object or value being traversed. + * @param collectedArgs The array collecting FromAIArgument objects. + */ + traverseNodeParameters(payload: unknown, collectedArgs: FromAIArgument[]) { + if (typeof payload === 'string') { + const fromAICalls = this.extractFromAICalls(payload); + fromAICalls.forEach((call) => collectedArgs.push(call)); + } else if (Array.isArray(payload)) { + payload.forEach((item: unknown) => this.traverseNodeParameters(item, collectedArgs)); + } else if (typeof payload === 'object' && payload !== null) { + Object.values(payload).forEach((value) => this.traverseNodeParameters(value, collectedArgs)); + } + } + + /** + * Extracts all $fromAI calls from a given string + * @param str The string to search for $fromAI calls. + * @returns An array of FromAIArgument objects. + * + * This method uses a regular expression to find the start of each $fromAI function call + * in the input string. It then employs a character-by-character parsing approach to + * accurately extract the arguments of each call, handling nested parentheses and quoted strings. + * + * The parsing process: + * 1. Finds the starting position of a $fromAI call using regex. + * 2. Iterates through characters, keeping track of parentheses depth and quote status. + * 3. Handles escaped characters within quotes to avoid premature quote closing. + * 4. Builds the argument string until the matching closing parenthesis is found. + * 5. Parses the extracted argument string into a FromAIArgument object. + * 6. Repeats the process for all $fromAI calls in the input string. + * + */ + extractFromAICalls(str: string): FromAIArgument[] { + const args: FromAIArgument[] = []; + // Regular expression to match the start of a $fromAI function call + const pattern = /\$fromAI\s*\(\s*/gi; + let match: RegExpExecArray | null; + + while ((match = pattern.exec(str)) !== null) { + const startIndex = match.index + match[0].length; + let current = startIndex; + let inQuotes = false; + let quoteChar = ''; + let parenthesesCount = 1; + let argsString = ''; + + // Parse the arguments string, handling nested parentheses and quotes + while (current < str.length && parenthesesCount > 0) { + const char = str[current]; + + if (inQuotes) { + // Handle characters inside quotes, including escaped characters + if (char === '\\' && current + 1 < str.length) { + argsString += char + str[current + 1]; + current += 2; + continue; + } + + if (char === quoteChar) { + inQuotes = false; + quoteChar = ''; + } + argsString += char; + } else { + // Handle characters outside quotes + if (['"', "'", '`'].includes(char)) { + inQuotes = true; + quoteChar = char; + } else if (char === '(') { + parenthesesCount++; + } else if (char === ')') { + parenthesesCount--; + } + + // Only add characters if we're still inside the main parentheses + if (parenthesesCount > 0 || char !== ')') { + argsString += char; + } + } + + current++; + } + + // If parentheses are balanced, parse the arguments + if (parenthesesCount === 0) { + try { + const parsedArgs = this.parseArguments(argsString); + args.push(parsedArgs); + } catch (error) { + // If parsing fails, throw an ApplicationError with details + throw new NodeOperationError( + this.ctx.getNode(), + `Failed to parse $fromAI arguments: ${argsString}: ${error}`, + ); + } + } else { + // Log an error if parentheses are unbalanced + throw new NodeOperationError( + this.ctx.getNode(), + `Unbalanced parentheses while parsing $fromAI call: ${str.slice(startIndex)}`, + ); + } + } + + return args; + } + + /** + * Parses the arguments of a single $fromAI function call. + * @param argsString The string containing the function arguments. + * @returns A FromAIArgument object. + */ + parseArguments(argsString: string): FromAIArgument { + // Split arguments by commas not inside quotes + const args: string[] = []; + let currentArg = ''; + let inQuotes = false; + let quoteChar = ''; + let escapeNext = false; + + for (let i = 0; i < argsString.length; i++) { + const char = argsString[i]; + + if (escapeNext) { + currentArg += char; + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (['"', "'", '`'].includes(char)) { + if (!inQuotes) { + inQuotes = true; + quoteChar = char; + currentArg += char; + } else if (char === quoteChar) { + inQuotes = false; + quoteChar = ''; + currentArg += char; + } else { + currentArg += char; + } + continue; + } + + if (char === ',' && !inQuotes) { + args.push(currentArg.trim()); + currentArg = ''; + continue; + } + + currentArg += char; + } + + if (currentArg) { + args.push(currentArg.trim()); + } + + // Remove surrounding quotes if present + const cleanArgs = args.map((arg) => { + const trimmed = arg.trim(); + if ( + (trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('`') && trimmed.endsWith('`')) || + (trimmed.startsWith('"') && trimmed.endsWith('"')) + ) { + return trimmed + .slice(1, -1) + .replace(/\\'/g, "'") + .replace(/\\`/g, '`') + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\'); + } + return trimmed; + }); + + const type = cleanArgs?.[2] || 'string'; + + if (!['string', 'number', 'boolean', 'json'].includes(type.toLowerCase())) { + throw new NodeOperationError(this.ctx.getNode(), `Invalid type: ${type}`); + } + + return { + key: cleanArgs[0] || '', + description: cleanArgs[1], + type: (cleanArgs?.[2] ?? 'string') as AllowedTypes, + defaultValue: this.parseDefaultValue(cleanArgs[3]), + }; + } + + /** + * Parses the default value, preserving its original type. + * @param value The default value as a string. + * @returns The parsed default value in its appropriate type. + */ + parseDefaultValue( + value: string | undefined, + ): string | number | boolean | Record | undefined { + if (value === undefined || value === '') return undefined; + const lowerValue = value.toLowerCase(); + if (lowerValue === 'true') return true; + if (lowerValue === 'false') return false; + if (!isNaN(Number(value))) return Number(value); + try { + return jsonParse(value); + } catch { + return value; + } + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts new file mode 100644 index 0000000000..2ce3c43556 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts @@ -0,0 +1,313 @@ +import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; +import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools'; +import get from 'lodash/get'; +import isObject from 'lodash/isObject'; +import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; +import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; +import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; +import type { + ExecuteWorkflowData, + ExecutionError, + IDataObject, + IExecuteWorkflowInfo, + INodeExecutionData, + INodeParameterResourceLocator, + ISupplyDataFunctions, + ITaskMetadata, + IWorkflowBase, + IWorkflowDataProxyData, + ResourceMapperValue, +} from 'n8n-workflow'; +import { jsonParse, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { z } from 'zod'; + +import type { FromAIArgument } from './FromAIParser'; +import { AIParametersParser } from './FromAIParser'; + +/** + Main class for creating the Workflow tool + Processes the node parameters and creates AI Agent tool capable of executing n8n workflows +*/ +export class WorkflowToolService { + // Determines if we should use input schema when creating the tool + private useSchema: boolean; + + // Sub-workflow id, pulled from referenced sub-workflow + private subWorkflowId: string | undefined; + + // Sub-workflow execution id, will be set after the sub-workflow is executed + private subExecutionId: string | undefined; + + constructor(private context: ISupplyDataFunctions) { + const subWorkflowInputs = this.context.getNode().parameters + .workflowInputs as ResourceMapperValue; + this.useSchema = (subWorkflowInputs?.schema ?? []).length > 0; + } + + // Creates the tool based on the provided parameters + async createTool({ + name, + description, + itemIndex, + }: { + name: string; + description: string; + itemIndex: number; + }): Promise { + // Handler for the tool execution, will be called when the tool is executed + // This function will execute the sub-workflow and return the response + const toolHandler = async ( + query: string | IDataObject, + runManager?: CallbackManagerForToolRun, + ): Promise => { + const { index } = this.context.addInputData(NodeConnectionType.AiTool, [ + [{ json: { query } }], + ]); + + try { + const response = await this.runFunction(query, itemIndex, runManager); + const processedResponse = this.handleToolResponse(response); + + // Once the sub-workflow is executed, add the output data to the context + // This will be used to link the sub-workflow execution in the parent workflow + let metadata: ITaskMetadata | undefined; + if (this.subExecutionId && this.subWorkflowId) { + metadata = { + subExecution: { + executionId: this.subExecutionId, + workflowId: this.subWorkflowId, + }, + }; + } + const json = jsonParse(processedResponse, { + fallbackValue: { response: processedResponse }, + }); + void this.context.addOutputData(NodeConnectionType.AiTool, index, [[{ json }]], metadata); + + return processedResponse; + } catch (error) { + const executionError = error as ExecutionError; + const errorResponse = `There was an error: "${executionError.message}"`; + void this.context.addOutputData(NodeConnectionType.AiTool, index, executionError); + return errorResponse; + } + }; + + // Create structured tool if input schema is provided + return this.useSchema + ? await this.createStructuredTool(name, description, toolHandler) + : new DynamicTool({ name, description, func: toolHandler }); + } + + private handleToolResponse(response: unknown): string { + if (typeof response === 'number') { + return response.toString(); + } + + if (isObject(response)) { + return JSON.stringify(response, null, 2); + } + + if (typeof response !== 'string') { + throw new NodeOperationError(this.context.getNode(), 'Wrong output type returned', { + description: `The response property should be a string, but it is an ${typeof response}`, + }); + } + + return response; + } + + /** + * Executes specified sub-workflow with provided inputs + */ + private async executeSubWorkflow( + workflowInfo: IExecuteWorkflowInfo, + items: INodeExecutionData[], + workflowProxy: IWorkflowDataProxyData, + runManager?: CallbackManagerForToolRun, + ): Promise<{ response: string; subExecutionId: string }> { + let receivedData: ExecuteWorkflowData; + try { + receivedData = await this.context.executeWorkflow( + workflowInfo, + items, + runManager?.getChild(), + { + parentExecution: { + executionId: workflowProxy.$execution.id, + workflowId: workflowProxy.$workflow.id, + }, + }, + ); + // Set sub-workflow execution id so it can be used in other places + this.subExecutionId = receivedData.executionId; + } catch (error) { + throw new NodeOperationError(this.context.getNode(), error as Error); + } + + const response: string | undefined = get(receivedData, 'data[0][0].json') as string | undefined; + if (response === undefined) { + throw new NodeOperationError( + this.context.getNode(), + 'There was an error: "The workflow did not return a response"', + ); + } + + return { response, subExecutionId: receivedData.executionId }; + } + + /** + * Gets the sub-workflow info based on the source and executes it. + * This function will be called as part of the tool execution (from the toolHandler) + */ + private async runFunction( + query: string | IDataObject, + itemIndex: number, + runManager?: CallbackManagerForToolRun, + ): Promise { + const source = this.context.getNodeParameter('source', itemIndex) as string; + const workflowProxy = this.context.getWorkflowDataProxy(0); + + const { workflowInfo } = await this.getSubWorkflowInfo(source, itemIndex, workflowProxy); + const rawData = this.prepareRawData(query, itemIndex); + const items = await this.prepareWorkflowItems(query, itemIndex, rawData); + + this.subWorkflowId = workflowInfo.id; + + const { response } = await this.executeSubWorkflow( + workflowInfo, + items, + workflowProxy, + runManager, + ); + return response; + } + + /** + * Gets the sub-workflow info based on the source (database or parameter) + */ + private async getSubWorkflowInfo( + source: string, + itemIndex: number, + workflowProxy: IWorkflowDataProxyData, + ): Promise<{ + workflowInfo: IExecuteWorkflowInfo; + subWorkflowId: string; + }> { + const workflowInfo: IExecuteWorkflowInfo = {}; + let subWorkflowId: string; + + if (source === 'database') { + const { value } = this.context.getNodeParameter( + 'workflowId', + itemIndex, + {}, + ) as INodeParameterResourceLocator; + workflowInfo.id = value as string; + subWorkflowId = workflowInfo.id; + } else if (source === 'parameter') { + const workflowJson = this.context.getNodeParameter('workflowJson', itemIndex) as string; + try { + workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase; + // subworkflow is same as parent workflow + subWorkflowId = workflowProxy.$workflow.id; + } catch (error) { + throw new NodeOperationError( + this.context.getNode(), + `The provided workflow is not valid JSON: "${(error as Error).message}"`, + { itemIndex }, + ); + } + } + + return { workflowInfo, subWorkflowId: subWorkflowId! }; + } + + private prepareRawData(query: string | IDataObject, itemIndex: number): IDataObject { + const rawData: IDataObject = { query }; + const workflowFieldsJson = this.context.getNodeParameter('fields.values', itemIndex, [], { + rawExpressions: true, + }) as SetField[]; + + // Copied from Set Node v2 + for (const entry of workflowFieldsJson) { + if (entry.type === 'objectValue' && (entry.objectValue as string).startsWith('=')) { + rawData[entry.name] = (entry.objectValue as string).replace(/^=+/, ''); + } + } + + return rawData; + } + + /** + * Prepares the sub-workflow items for execution + */ + private async prepareWorkflowItems( + query: string | IDataObject, + itemIndex: number, + rawData: IDataObject, + ): Promise { + const options: SetNodeOptions = { include: 'all' }; + let jsonData = typeof query === 'object' ? query : { query }; + + if (this.useSchema) { + const currentWorkflowInputs = getCurrentWorkflowInputData.call(this.context); + jsonData = currentWorkflowInputs[itemIndex].json; + } + + const newItem = await manual.execute.call( + this.context, + { json: jsonData }, + itemIndex, + options, + rawData, + this.context.getNode(), + ); + + return [newItem] as INodeExecutionData[]; + } + + /** + * Create structured tool by parsing the sub-workflow input schema + */ + private async createStructuredTool( + name: string, + description: string, + func: (query: string | IDataObject, runManager?: CallbackManagerForToolRun) => Promise, + ): Promise { + const fromAIParser = new AIParametersParser(this.context); + const collectedArguments = await this.extractFromAIParameters(fromAIParser); + + // If there are no `fromAI` arguments, fallback to creating a simple tool + if (collectedArguments.length === 0) { + return new DynamicTool({ name, description, func }); + } + + // Otherwise, prepare Zod schema and create a structured tool + const schema = this.createZodSchema(collectedArguments, fromAIParser); + return new DynamicStructuredTool({ schema, name, description, func }); + } + + private async extractFromAIParameters( + fromAIParser: AIParametersParser, + ): Promise { + const collectedArguments: FromAIArgument[] = []; + fromAIParser.traverseNodeParameters(this.context.getNode().parameters, collectedArguments); + + const uniqueArgsMap = new Map(); + for (const arg of collectedArguments) { + uniqueArgsMap.set(arg.key, arg); + } + + return Array.from(uniqueArgsMap.values()); + } + + private createZodSchema(args: FromAIArgument[], parser: AIParametersParser): z.ZodObject { + const schemaObj = args.reduce((acc: Record, placeholder) => { + acc[placeholder.key] = parser.generateZodSchema(placeholder); + return acc; + }, {}); + + return z.object(schemaObj).required(); + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts new file mode 100644 index 0000000000..469a7d6d4c --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts @@ -0,0 +1,151 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ +import { NodeConnectionType, type INodeTypeDescription } from 'n8n-workflow'; + +import { getConnectionHintNoticeField } from '../../../../utils/sharedFields'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Call n8n Workflow Tool', + name: 'toolWorkflow', + icon: 'fa:network-wired', + group: ['transform'], + description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', + defaults: { + name: 'Call n8n Workflow Tool', + }, + version: [2], + inputs: [], + outputs: [NodeConnectionType.AiTool], + outputNames: ['Tool'], + properties: [ + getConnectionHintNoticeField([NodeConnectionType.AiAgent]), + { + displayName: + 'See an example of a workflow to suggest meeting slots using AI here.', + name: 'noticeTemplateExample', + type: 'notice', + default: '', + }, + { + 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', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + placeholder: + 'Call this tool to get a random color. The input should be a string with comma separated names of colors to exclude.', + typeOptions: { + rows: 3, + }, + }, + + { + displayName: + 'This tool will call the workflow you define below, and look in the last node for the response. The workflow needs to start with an Execute Workflow trigger', + name: 'executeNotice', + type: 'notice', + default: '', + }, + + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + description: 'Load the workflow from the database by ID', + }, + { + name: 'Define Below', + value: 'parameter', + description: 'Pass the JSON code of a workflow', + }, + ], + default: 'database', + description: 'Where to get the workflow to execute from', + }, + + // ---------------------------------- + // source:database + // ---------------------------------- + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { + show: { + source: ['database'], + }, + }, + default: '', + required: true, + }, + // ----------------------------------------------- + // Resource mapper for workflow inputs + // ----------------------------------------------- + { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + type: 'resourceMapper', + noDataExpression: true, + default: { + mappingMode: 'defineBelow', + value: null, + }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['workflowId.value'], + resourceMapper: { + localResourceMapperMethod: 'loadWorkflowInputMappings', + valuesLabel: 'Workflow Inputs', + mode: 'map', + fieldWords: { + singular: 'workflow input', + plural: 'workflow inputs', + }, + addAllFields: true, + multiKeyMatch: false, + supportAutoMap: false, + }, + }, + displayOptions: { + show: { + source: ['database'], + }, + hide: { + workflowId: [''], + }, + }, + }, + // ---------------------------------- + // source:parameter + // ---------------------------------- + { + displayName: 'Workflow JSON', + name: 'workflowJson', + type: 'json', + typeOptions: { + rows: 10, + }, + displayOptions: { + show: { + source: ['parameter'], + }, + }, + default: '\n\n\n\n\n\n\n\n\n', + required: true, + description: 'The workflow JSON code to execute', + }, + ], +}; diff --git a/packages/cli/src/controllers/dynamic-node-parameters.controller.ts b/packages/cli/src/controllers/dynamic-node-parameters.controller.ts index f3df53d95b..2858fd99ca 100644 --- a/packages/cli/src/controllers/dynamic-node-parameters.controller.ts +++ b/packages/cli/src/controllers/dynamic-node-parameters.controller.ts @@ -93,6 +93,22 @@ export class DynamicNodeParametersController { ); } + @Post('/local-resource-mapper-fields') + async getLocalResourceMappingFields(req: DynamicNodeParametersRequest.ResourceMapperFields) { + const { path, methodName, currentNodeParameters, nodeTypeAndVersion } = req.body; + + if (!methodName) throw new BadRequestError('Missing `methodName` in request body'); + + const additionalData = await getBase(req.user.id, currentNodeParameters); + + return await this.service.getLocalResourceMappingFields( + methodName, + path, + additionalData, + nodeTypeAndVersion, + ); + } + @Post('/action-result') async getActionResult( req: DynamicNodeParametersRequest.ActionResult, diff --git a/packages/cli/src/services/dynamic-node-parameters.service.ts b/packages/cli/src/services/dynamic-node-parameters.service.ts index a20d63b5fa..65c40ef0b6 100644 --- a/packages/cli/src/services/dynamic-node-parameters.service.ts +++ b/packages/cli/src/services/dynamic-node-parameters.service.ts @@ -1,4 +1,4 @@ -import { LoadOptionsContext, RoutingNode } from 'n8n-core'; +import { LoadOptionsContext, RoutingNode, LocalLoadOptionsContext } from 'n8n-core'; import type { ILoadOptions, ILoadOptionsFunctions, @@ -17,15 +17,43 @@ import type { INodeTypeNameVersion, NodeParameterValueType, IDataObject, + ILocalLoadOptionsFunctions, } from 'n8n-workflow'; import { Workflow, ApplicationError } from 'n8n-workflow'; import { Service } from 'typedi'; import { NodeTypes } from '@/node-types'; +import { WorkflowLoaderService } from './workflow-loader.service'; + +type LocalResourceMappingMethod = ( + this: ILocalLoadOptionsFunctions, +) => Promise; +type ListSearchMethod = ( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +) => Promise; +type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise; +type ActionHandlerMethod = ( + this: ILoadOptionsFunctions, + payload?: string, +) => Promise; +type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise; + +type NodeMethod = + | LocalResourceMappingMethod + | ListSearchMethod + | LoadOptionsMethod + | ActionHandlerMethod + | ResourceMappingMethod; + @Service() export class DynamicNodeParametersService { - constructor(private nodeTypes: NodeTypes) {} + constructor( + private nodeTypes: NodeTypes, + private workflowLoaderService: WorkflowLoaderService, + ) {} /** Returns the available options via a predefined method */ async getOptionsViaMethodName( @@ -40,6 +68,8 @@ export class DynamicNodeParametersService { const method = this.getMethod('loadOptions', methodName, nodeType); const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); const thisArgs = this.getThisArg(path, additionalData, workflow); + // Need to use untyped call since `this` usage is widespread and we don't have `strictBindCallApply` + // enabled in `tsconfig.json` // eslint-disable-next-line @typescript-eslint/no-unsafe-return return method.call(thisArgs); } @@ -157,6 +187,20 @@ export class DynamicNodeParametersService { return method.call(thisArgs); } + /** Returns the available workflow input mapping fields for the ResourceMapper component */ + async getLocalResourceMappingFields( + methodName: string, + path: string, + additionalData: IWorkflowExecuteAdditionalData, + nodeTypeAndVersion: INodeTypeNameVersion, + ): Promise { + const nodeType = this.getNodeType(nodeTypeAndVersion); + const method = this.getMethod('localResourceMapping', methodName, nodeType); + const thisArgs = this.getLocalLoadOptionsContext(path, additionalData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return method.call(thisArgs); + } + /** Returns the result of the action handler */ async getActionResult( handler: string, @@ -179,33 +223,34 @@ export class DynamicNodeParametersService { type: 'resourceMapping', methodName: string, nodeType: INodeType, - ): (this: ILoadOptionsFunctions) => Promise; + ): ResourceMappingMethod; private getMethod( - type: 'listSearch', + type: 'localResourceMapping', methodName: string, nodeType: INodeType, - ): ( - this: ILoadOptionsFunctions, - filter?: string | undefined, - paginationToken?: string | undefined, - ) => Promise; + ): LocalResourceMappingMethod; + private getMethod(type: 'listSearch', methodName: string, nodeType: INodeType): ListSearchMethod; private getMethod( type: 'loadOptions', methodName: string, nodeType: INodeType, - ): (this: ILoadOptionsFunctions) => Promise; + ): LoadOptionsMethod; private getMethod( type: 'actionHandler', methodName: string, nodeType: INodeType, - ): (this: ILoadOptionsFunctions, payload?: string) => Promise; - + ): ActionHandlerMethod; private getMethod( - type: 'resourceMapping' | 'listSearch' | 'loadOptions' | 'actionHandler', + type: + | 'resourceMapping' + | 'localResourceMapping' + | 'listSearch' + | 'loadOptions' + | 'actionHandler', methodName: string, nodeType: INodeType, - ) { - const method = nodeType.methods?.[type]?.[methodName]; + ): NodeMethod { + const method = nodeType.methods?.[type]?.[methodName] as NodeMethod; if (typeof method !== 'function') { throw new ApplicationError('Node type does not have method defined', { tags: { nodeType: nodeType.description.name }, @@ -253,4 +298,16 @@ export class DynamicNodeParametersService { const node = workflow.nodes['Temp-Node']; return new LoadOptionsContext(workflow, node, additionalData, path); } + + private getLocalLoadOptionsContext( + path: string, + additionalData: IWorkflowExecuteAdditionalData, + ): ILocalLoadOptionsFunctions { + return new LocalLoadOptionsContext( + this.nodeTypes, + additionalData, + path, + this.workflowLoaderService, + ); + } } diff --git a/packages/cli/src/services/workflow-loader.service.ts b/packages/cli/src/services/workflow-loader.service.ts new file mode 100644 index 0000000000..ca1a9ff48c --- /dev/null +++ b/packages/cli/src/services/workflow-loader.service.ts @@ -0,0 +1,19 @@ +import { ApplicationError, type IWorkflowBase, type IWorkflowLoader } from 'n8n-workflow'; +import { Service } from 'typedi'; + +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; + +@Service() +export class WorkflowLoaderService implements IWorkflowLoader { + constructor(private readonly workflowRepository: WorkflowRepository) {} + + async get(workflowId: string): Promise { + const workflow = await this.workflowRepository.findById(workflowId); + + if (!workflow) { + throw new ApplicationError(`Failed to find workflow with ID "${workflowId}"`); + } + + return workflow; + } +} diff --git a/packages/core/src/CreateNodeAsTool.ts b/packages/core/src/CreateNodeAsTool.ts index a84c564210..da34b377df 100644 --- a/packages/core/src/CreateNodeAsTool.ts +++ b/packages/core/src/CreateNodeAsTool.ts @@ -17,6 +17,9 @@ type ParserOptions = { handleToolInvocation: (toolArgs: IDataObject) => Promise; }; +// This file is temporarily duplicated in `packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/FromAIParser.ts` +// Please apply any changes in both files + /** * AIParametersParser * diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 252694fd1f..f340c8da67 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -1197,7 +1197,7 @@ export class WorkflowExecute { }); if (workflowIssues !== null) { throw new WorkflowOperationError( - 'The workflow has issues and can for that reason not be executed. Please fix them first.', + 'The workflow has issues and cannot be executed for that reason. Please fix them first.', ); } diff --git a/packages/core/src/node-execution-context/index.ts b/packages/core/src/node-execution-context/index.ts index 64088af72e..c3bcebbd44 100644 --- a/packages/core/src/node-execution-context/index.ts +++ b/packages/core/src/node-execution-context/index.ts @@ -3,6 +3,7 @@ export { ExecuteContext } from './execute-context'; export { ExecuteSingleContext } from './execute-single-context'; export { HookContext } from './hook-context'; export { LoadOptionsContext } from './load-options-context'; +export { LocalLoadOptionsContext } from './local-load-options-context'; export { PollContext } from './poll-context'; // eslint-disable-next-line import/no-cycle export { SupplyDataContext } from './supply-data-context'; diff --git a/packages/core/src/node-execution-context/local-load-options-context.ts b/packages/core/src/node-execution-context/local-load-options-context.ts new file mode 100644 index 0000000000..39456ff966 --- /dev/null +++ b/packages/core/src/node-execution-context/local-load-options-context.ts @@ -0,0 +1,70 @@ +import lodash from 'lodash'; +import { ApplicationError, Workflow } from 'n8n-workflow'; +import type { + INodeParameterResourceLocator, + IWorkflowExecuteAdditionalData, + NodeParameterValueType, + ILocalLoadOptionsFunctions, + IWorkflowLoader, + IWorkflowNodeContext, + INodeTypes, +} from 'n8n-workflow'; + +import { LoadWorkflowNodeContext } from './workflow-node-context'; + +export class LocalLoadOptionsContext implements ILocalLoadOptionsFunctions { + constructor( + private nodeTypes: INodeTypes, + private additionalData: IWorkflowExecuteAdditionalData, + private path: string, + private workflowLoader: IWorkflowLoader, + ) {} + + async getWorkflowNodeContext(nodeType: string): Promise { + const { value: workflowId } = this.getCurrentNodeParameter( + 'workflowId', + ) as INodeParameterResourceLocator; + + if (typeof workflowId !== 'string' || !workflowId) { + throw new ApplicationError(`No workflowId parameter defined on node of type "${nodeType}"!`); + } + + const dbWorkflow = await this.workflowLoader.get(workflowId); + + const selectedWorkflowNode = dbWorkflow.nodes.find((node) => node.type === nodeType); + + if (selectedWorkflowNode) { + const selectedSingleNodeWorkflow = new Workflow({ + nodes: [selectedWorkflowNode], + connections: {}, + active: false, + nodeTypes: this.nodeTypes, + }); + + const workflowAdditionalData = { + ...this.additionalData, + currentNodeParameters: selectedWorkflowNode.parameters, + }; + + return new LoadWorkflowNodeContext( + selectedSingleNodeWorkflow, + selectedWorkflowNode, + workflowAdditionalData, + ); + } + + return null; + } + + getCurrentNodeParameter(parameterPath: string): NodeParameterValueType | object | undefined { + const nodeParameters = this.additionalData.currentNodeParameters; + + if (parameterPath.startsWith('&')) { + parameterPath = `${this.path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`; + } + + const returnData = lodash.get(nodeParameters, parameterPath); + + return returnData; + } +} diff --git a/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts b/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts index 2792199807..adac8c3a78 100644 --- a/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts +++ b/packages/core/src/node-execution-context/utils/validateValueAgainstSchema.ts @@ -53,15 +53,19 @@ const validateResourceMapperValue = ( }; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (schemaEntry?.type) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access valueOptions: schemaEntry.options, + strict: !resourceMapperField.attemptToConvertTypes, + parseStrings: !!resourceMapperField.convertFieldsToString, }); + if (!validationResult.valid) { - return { ...validationResult, fieldName: key }; + if (!resourceMapperField.ignoreTypeMismatchErrors) { + return { ...validationResult, fieldName: key }; + } else { + paramValues[key] = resolvedValue; + } } else { // If it's valid, set the casted value paramValues[key] = validationResult.newValue; diff --git a/packages/core/src/node-execution-context/workflow-node-context.ts b/packages/core/src/node-execution-context/workflow-node-context.ts new file mode 100644 index 0000000000..18de159e4b --- /dev/null +++ b/packages/core/src/node-execution-context/workflow-node-context.ts @@ -0,0 +1,36 @@ +import type { + IGetNodeParameterOptions, + INode, + IWorkflowExecuteAdditionalData, + Workflow, + IWorkflowNodeContext, +} from 'n8n-workflow'; + +import { NodeExecutionContext } from './node-execution-context'; + +export class LoadWorkflowNodeContext extends NodeExecutionContext implements IWorkflowNodeContext { + // Note that this differs from and does not shadow the function with the + // same name in `NodeExecutionContext`, as it has the `itemIndex` parameter + readonly getNodeParameter: IWorkflowNodeContext['getNodeParameter']; + + constructor(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData) { + super(workflow, node, additionalData, 'internal'); + { + // We need to cast due to the overloaded IWorkflowNodeContext::getNodeParameter function + // Which would require us to replicate all overload return types, as TypeScript offers + // no convenient solution to refer to a set of overloads. + this.getNodeParameter = (( + parameterName: string, + itemIndex: number, + fallbackValue?: unknown, + options?: IGetNodeParameterOptions, + ) => + this._getNodeParameter( + parameterName, + itemIndex, + fallbackValue, + options, + )) as IWorkflowNodeContext['getNodeParameter']; + } + } +} diff --git a/packages/editor-ui/src/api/nodeTypes.ts b/packages/editor-ui/src/api/nodeTypes.ts index f4d516aaef..ec3e2bdba5 100644 --- a/packages/editor-ui/src/api/nodeTypes.ts +++ b/packages/editor-ui/src/api/nodeTypes.ts @@ -59,6 +59,18 @@ export async function getResourceMapperFields( ); } +export async function getLocalResourceMapperFields( + context: IRestApiContext, + sendData: DynamicNodeParameters.ResourceMapperFieldsRequest, +): Promise { + return await makeRestApiRequest( + context, + 'POST', + '/dynamic-node-parameters/local-resource-mapper-fields', + sendData, + ); +} + export async function getNodeParameterActionResult( context: IRestApiContext, sendData: DynamicNodeParameters.ActionResultRequest, diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 1ee841323d..1a5740979d 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -1431,6 +1431,7 @@ onUpdated(async () => { :key="option.value.toString()" :value="option.value" :label="getOptionsOptionDisplayName(option)" + data-test-id="parameter-input-item" >
{ expect(queryByTestId('matching-column-select')).not.toBeInTheDocument(); }); + it('renders map mode properly', async () => { + const { getByTestId, queryByTestId } = renderComponent( + { + props: { + parameter: { + typeOptions: { + resourceMapper: { + mode: 'map', + }, + }, + }, + }, + }, + { merge: true }, + ); + await waitAllPromises(); + expect(getByTestId('resource-mapper-container')).toBeInTheDocument(); + // This mode doesn't render matching column selector + expect(queryByTestId('matching-column-select')).not.toBeInTheDocument(); + }); + it('renders multi-key match selector properly', async () => { const { container, getByTestId } = renderComponent( { @@ -201,7 +222,7 @@ describe('ResourceMapper.vue', () => { expect( getByText('Look for incoming data that matches the foos in the service'), ).toBeInTheDocument(); - expect(getByText('Foos to Match On')).toBeInTheDocument(); + expect(getByText('Foos to match on')).toBeInTheDocument(); expect( getByText( 'The foos to use when matching rows in the service to the input items of this node. Usually an ID.', diff --git a/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue b/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue index 0b444dd13f..546e33cdc3 100644 --- a/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue +++ b/packages/editor-ui/src/components/ResourceMapper/MappingFields.vue @@ -21,7 +21,14 @@ import { parseResourceMapperFieldName, } from '@/utils/nodeTypesUtils'; import { useNodeSpecificationValues } from '@/composables/useNodeSpecificationValues'; -import { N8nIconButton, N8nInputLabel, N8nOption, N8nSelect, N8nTooltip } from 'n8n-design-system'; +import { + N8nIcon, + N8nIconButton, + N8nInputLabel, + N8nOption, + N8nSelect, + N8nTooltip, +} from 'n8n-design-system'; import { useI18n } from '@/composables/useI18n'; interface Props { @@ -37,11 +44,13 @@ interface Props { refreshInProgress: boolean; teleported?: boolean; isReadOnly?: boolean; + isDataStale?: boolean; } const props = withDefaults(defineProps(), { teleported: true, isReadOnly: false, + isDataStale: false, }); const FORCE_TEXT_INPUT_FOR_TYPES: FieldType[] = ['time', 'object', 'array']; @@ -310,6 +319,27 @@ defineExpose({ :value="props.paramValue" @update:model-value="onParameterActionSelected" /> +
+ + + + + +
@@ -360,7 +390,7 @@ defineExpose({ :title=" locale.baseText('resourceMapper.removeField', { interpolate: { - fieldWord: singularFieldWordCapitalized, + fieldWord: singularFieldWord, }, }) " @@ -391,7 +421,7 @@ defineExpose({ diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index b09354ed00..7487f1d8d4 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -7,7 +7,9 @@ import type { INodeParameters, INodeProperties, INodeTypeDescription, + NodeParameterValueType, ResourceMapperField, + ResourceMapperFields, ResourceMapperValue, } from 'n8n-workflow'; import { NodeHelpers } from 'n8n-workflow'; @@ -15,11 +17,17 @@ import { computed, onMounted, reactive, watch } from 'vue'; import MappingModeSelect from './MappingModeSelect.vue'; import MatchingColumnsSelect from './MatchingColumnsSelect.vue'; import MappingFields from './MappingFields.vue'; -import { fieldCannotBeDeleted, parseResourceMapperFieldName } from '@/utils/nodeTypesUtils'; +import { + fieldCannotBeDeleted, + isResourceMapperFieldListStale, + parseResourceMapperFieldName, +} from '@/utils/nodeTypesUtils'; import { isFullExecutionResponse, isResourceMapperValue } from '@/utils/typeGuards'; import { i18n as locale } from '@/plugins/i18n'; import { useNDVStore } from '@/stores/ndv.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useDocumentVisibility } from '@/composables/useDocumentVisibility'; +import { N8nButton, N8nCallout } from 'n8n-design-system'; type Props = { parameter: INodeProperties; @@ -42,6 +50,8 @@ const props = withDefaults(defineProps(), { isReadOnly: false, }); +const { onDocumentVisible } = useDocumentVisibility(); + const emit = defineEmits<{ valueChanged: [value: IUpdateInformation]; }>(); @@ -52,11 +62,18 @@ const state = reactive({ value: {}, matchingColumns: [] as string[], schema: [] as ResourceMapperField[], + ignoreTypeMismatchErrors: false, + attemptToConvertTypes: false, + // This should always be true if `showTypeConversionOptions` is provided + // It's used to avoid accepting any value as string without casting it + // Which is the legacy behavior without these type options. + convertFieldsToString: false, } as ResourceMapperValue, parameterValues: {} as INodeParameters, loading: false, refreshInProgress: false, // Shows inline loader when refreshing fields loadingError: false, + hasStaleFields: false, }); // Reload fields to map when dependent parameters change @@ -76,6 +93,21 @@ watch( }, ); +onDocumentVisible(async () => { + await checkStaleFields(); +}); + +async function checkStaleFields(): Promise { + const fetchedFields = await fetchFields(); + if (fetchedFields) { + const isSchemaStale = isResourceMapperFieldListStale( + state.paramValue.schema, + fetchedFields.fields, + ); + state.hasStaleFields = isSchemaStale; + } +} + // Reload fields to map when node is executed watch( () => workflowsStore.getWorkflowExecution, @@ -97,6 +129,10 @@ onMounted(async () => { ...state.parameterValues, parameters: props.node.parameters, }; + + if (showTypeConversionOptions.value) { + state.paramValue.convertFieldsToString = true; + } } const params = state.parameterValues.parameters as INodeParameters; const parameterName = props.parameter.name; @@ -138,6 +174,8 @@ onMounted(async () => { if (!hasSchema) { // Only fetch a schema if it's not already set await initFetching(); + } else { + await checkStaleFields(); } // Set default values if this is the first time the parameter is being set if (!state.paramValue.value) { @@ -161,11 +199,19 @@ const showMappingModeSelect = computed(() => { return props.parameter.typeOptions?.resourceMapper?.supportAutoMap !== false; }); +const showTypeConversionOptions = computed(() => { + return props.parameter.typeOptions?.resourceMapper?.showTypeConversionOptions === true; +}); + +const hasFields = computed(() => { + return state.paramValue.schema.length > 0; +}); + const showMatchingColumnsSelector = computed(() => { return ( !state.loading && - props.parameter.typeOptions?.resourceMapper?.mode !== 'add' && - state.paramValue.schema.length > 0 + ['upsert', 'update'].includes(props.parameter.typeOptions?.resourceMapper?.mode ?? '') && + hasFields.value ); }); @@ -174,7 +220,7 @@ const showMappingFields = computed(() => { state.paramValue.mappingMode === 'defineBelow' && !state.loading && !state.loadingError && - state.paramValue.schema.length > 0 && + hasFields.value && hasAvailableMatchingColumns.value ); }); @@ -190,6 +236,10 @@ const matchingColumns = computed(() => { }); const hasAvailableMatchingColumns = computed(() => { + // 'map' mode doesn't require matching columns + if (resourceMapperMode.value === 'map') { + return true; + } if (resourceMapperMode.value !== 'add' && resourceMapperMode.value !== 'upsert') { return ( state.paramValue.schema.filter( @@ -227,10 +277,11 @@ async function initFetching(inlineLoading = false): Promise { state.loading = true; } try { - await loadFieldsToMap(); + await loadAndSetFieldsToMap(); if (!state.paramValue.matchingColumns || state.paramValue.matchingColumns.length === 0) { onMatchingColumnsChanged(defaultSelectedMatchingColumns.value); } + state.hasStaleFields = false; } catch (error) { state.loadingError = true; } finally { @@ -239,19 +290,13 @@ async function initFetching(inlineLoading = false): Promise { } } -async function loadFieldsToMap(): Promise { +const createRequestParams = (methodName: string) => { if (!props.node) { return; } - - const methodName = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod; - if (typeof methodName !== 'string') { - return; - } - const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = { nodeTypeAndVersion: { - name: props.node?.type, + name: props.node.type, version: props.node.typeVersion, }, currentNodeParameters: resolveRequiredParameters( @@ -262,7 +307,38 @@ async function loadFieldsToMap(): Promise { methodName, credentials: props.node.credentials, }; - const fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams); + + return requestParams; +}; + +async function fetchFields(): Promise { + const { resourceMapperMethod, localResourceMapperMethod } = + props.parameter.typeOptions?.resourceMapper ?? {}; + + let fetchedFields = null; + + if (typeof resourceMapperMethod === 'string') { + const requestParams = createRequestParams( + resourceMapperMethod, + ) as DynamicNodeParameters.ResourceMapperFieldsRequest; + fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams); + } else if (typeof localResourceMapperMethod === 'string') { + const requestParams = createRequestParams( + localResourceMapperMethod, + ) as DynamicNodeParameters.ResourceMapperFieldsRequest; + + fetchedFields = await nodeTypesStore.getLocalResourceMapperFields(requestParams); + } + return fetchedFields; +} + +async function loadAndSetFieldsToMap(): Promise { + if (!props.node) { + return; + } + + const fetchedFields = await fetchFields(); + if (fetchedFields !== null) { const newSchema = fetchedFields.fields.map((field) => { const existingField = state.paramValue.schema.find((f) => f.id === field.id); @@ -531,11 +607,26 @@ defineExpose({ :teleported="teleported" :refresh-in-progress="state.refreshInProgress" :is-read-only="isReadOnly" + :is-data-stale="state.hasStaleFields" @field-value-changed="fieldValueChanged" @remove-field="removeField" @add-field="addField" @refresh-field-list="initFetching(true)" /> + + {{ locale.baseText('resourceMapper.staleDataWarning.notice') }} + + @@ -548,5 +639,49 @@ defineExpose({ }) }} +
+ + +
+ + diff --git a/packages/editor-ui/src/composables/useDocumentVisibility.ts b/packages/editor-ui/src/composables/useDocumentVisibility.ts new file mode 100644 index 0000000000..47af96309f --- /dev/null +++ b/packages/editor-ui/src/composables/useDocumentVisibility.ts @@ -0,0 +1,52 @@ +import type { Ref } from 'vue'; +import { ref, onMounted, onUnmounted } from 'vue'; + +type VisibilityHandler = () => void; + +type DocumentVisibilityResult = { + isVisible: Ref; + onDocumentVisible: (handler: VisibilityHandler) => void; + onDocumentHidden: (handler: VisibilityHandler) => void; +}; + +export function useDocumentVisibility(): DocumentVisibilityResult { + const isVisible = ref(!document.hidden); + const visibleHandlers = ref([]); + const hiddenHandlers = ref([]); + + const onVisibilityChange = (): void => { + const newVisibilityState = !document.hidden; + isVisible.value = newVisibilityState; + + if (newVisibilityState) { + visibleHandlers.value.forEach((handler) => handler()); + } else { + hiddenHandlers.value.forEach((handler) => handler()); + } + }; + + const onDocumentVisible = (handler: VisibilityHandler): void => { + visibleHandlers.value.push(handler); + }; + + const onDocumentHidden = (handler: VisibilityHandler): void => { + hiddenHandlers.value.push(handler); + }; + + onMounted((): void => { + document.addEventListener('visibilitychange', onVisibilityChange); + }); + + onUnmounted((): void => { + document.removeEventListener('visibilitychange', onVisibilityChange); + // Clear handlers on unmount + visibleHandlers.value = []; + hiddenHandlers.value = []; + }); + + return { + isVisible, + onDocumentVisible, + onDocumentHidden, + }; +} diff --git a/packages/editor-ui/src/constants.workflows.ts b/packages/editor-ui/src/constants.workflows.ts index 52a6881df8..2116a471f9 100644 --- a/packages/editor-ui/src/constants.workflows.ts +++ b/packages/editor-ui/src/constants.workflows.ts @@ -175,7 +175,8 @@ export const SAMPLE_SUBWORKFLOW_WORKFLOW: WorkflowDataWithTemplateId = { nodes: [ { id: 'c055762a-8fe7-4141-a639-df2372f30060', - name: 'Execute Workflow Trigger', + typeVersion: 1.1, + name: 'Workflow Input Trigger', type: 'n8n-nodes-base.executeWorkflowTrigger', position: [260, 340], parameters: {}, @@ -189,7 +190,7 @@ export const SAMPLE_SUBWORKFLOW_WORKFLOW: WorkflowDataWithTemplateId = { }, ] as INodeUi[], connections: { - 'Execute Workflow Trigger': { + 'Workflow Input Trigger': { main: [ [ { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 638000b219..3af5aa9111 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -58,6 +58,7 @@ "generic.yes": "Yes", "generic.no": "No", "generic.rating": "Rating", + "generic.refresh": "Refresh", "generic.retry": "Retry", "generic.error": "Something went wrong", "generic.settings": "Settings", @@ -1572,7 +1573,7 @@ "resourceMapper.fetchingFields.message": "Fetching {fieldWord}", "resourceMapper.fetchingFields.errorMessage": "Can't get {fieldWord}.", "resourceMapper.fetchingFields.noFieldsFound": "No {fieldWord} found in {serviceName}.", - "resourceMapper.columnsToMatchOn.label": "{fieldWord} to Match On", + "resourceMapper.columnsToMatchOn.label": "{fieldWord} to match on", "resourceMapper.columnsToMatchOn.multi.description": "The {fieldWord} to use when matching rows in {nodeDisplayName} to the input items of this node. Usually an ID.", "resourceMapper.columnsToMatchOn.single.description": "The {fieldWord} to use when matching rows in {nodeDisplayName} to the input items of this node. Usually an ID.", "resourceMapper.columnsToMatchOn.tooltip": "The {fieldWord} to compare when finding the rows to update", @@ -1583,11 +1584,17 @@ "resourceMapper.usingToMatch.description": "This {fieldWord} won't be updated and can't be removed, as it's used for matching", "resourceMapper.removeField": "Remove {fieldWord}", "resourceMapper.mandatoryField.title": "This {fieldWord} is mandatory and can’t be removed", - "resourceMapper.addFieldToSend": "Add {fieldWord} to Send", + "resourceMapper.addFieldToSend": "Add {fieldWord} to send", "resourceMapper.matching.title": "This {fieldWord} is used for matching and can’t be removed", "resourceMapper.addAllFields": "Add All {fieldWord}", "resourceMapper.removeAllFields": "Remove All {fieldWord}", "resourceMapper.refreshFieldList": "Refresh {fieldWord} List", + "resourceMapper.staleDataWarning.tooltip": "{fieldWord} are outdated. Refresh to see the changes.", + "resourceMapper.staleDataWarning.notice": "Refresh to see the updated fields", + "resourceMapper.attemptToConvertTypes.displayName": "Attempt to convert types", + "resourceMapper.attemptToConvertTypes.description": "Attempt to convert types when mapping fields", + "resourceMapper.ignoreTypeMismatchErrors.displayName": "Ignore type mismatch errors", + "resourceMapper.ignoreTypeMismatchErrors.description": "Whether type mismatches should be ignored, rather than returning an Error", "runData.openSubExecution": "Inspect Sub-Execution {id}", "runData.openParentExecution": "Inspect Parent Execution {id}", "runData.emptyItemHint": "This is an item, but it's empty.", diff --git a/packages/editor-ui/src/stores/nodeTypes.store.ts b/packages/editor-ui/src/stores/nodeTypes.store.ts index 7a8e66aab0..06917edfaa 100644 --- a/packages/editor-ui/src/stores/nodeTypes.store.ts +++ b/packages/editor-ui/src/stores/nodeTypes.store.ts @@ -302,6 +302,16 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => { } }; + const getLocalResourceMapperFields = async ( + sendData: DynamicNodeParameters.ResourceMapperFieldsRequest, + ) => { + try { + return await nodeTypesApi.getLocalResourceMapperFields(rootStore.restApiContext, sendData); + } catch (error) { + return null; + } + }; + const getNodeParameterActionResult = async ( sendData: DynamicNodeParameters.ActionResultRequest, ) => { @@ -326,6 +336,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => { visibleNodeTypesByInputConnectionTypeNames, isConfigurableNode, getResourceMapperFields, + getLocalResourceMapperFields, getNodeParameterActionResult, getResourceLocatorResults, getNodeParameterOptions, diff --git a/packages/editor-ui/src/utils/nodeTypeUtils.test.ts b/packages/editor-ui/src/utils/nodeTypeUtils.test.ts new file mode 100644 index 0000000000..8eb9c47118 --- /dev/null +++ b/packages/editor-ui/src/utils/nodeTypeUtils.test.ts @@ -0,0 +1,75 @@ +import type { ResourceMapperField } from 'n8n-workflow'; +import { isResourceMapperFieldListStale } from './nodeTypesUtils'; + +describe('isResourceMapperFieldListStale', () => { + const baseField: ResourceMapperField = { + id: 'test', + displayName: 'test', + required: false, + defaultMatch: false, + display: true, + canBeUsedToMatch: true, + type: 'string', + }; + + // Test property changes + test.each([ + [ + 'displayName', + { ...baseField }, + { ...baseField, displayName: 'changed' } as ResourceMapperField, + ], + ['required', { ...baseField }, { ...baseField, required: true } as ResourceMapperField], + ['defaultMatch', { ...baseField }, { ...baseField, defaultMatch: true } as ResourceMapperField], + ['display', { ...baseField }, { ...baseField, display: false }], + [ + 'canBeUsedToMatch', + { ...baseField }, + { ...baseField, canBeUsedToMatch: false } as ResourceMapperField, + ], + ['type', { ...baseField }, { ...baseField, type: 'number' } as ResourceMapperField], + ])('returns true when %s changes', (_property, oldField, newField) => { + expect(isResourceMapperFieldListStale([oldField], [newField])).toBe(true); + }); + + // Test different array lengths + test.each([ + ['empty vs non-empty', [], [baseField]], + ['non-empty vs empty', [baseField], []], + ['one vs two fields', [baseField], [baseField, { ...baseField, id: 'test2' }]], + ])('returns true for different lengths: %s', (_scenario, oldFields, newFields) => { + expect(isResourceMapperFieldListStale(oldFields, newFields)).toBe(true); + }); + + // Test identical cases + test.each([ + ['empty arrays', [], []], + ['single field', [baseField], [{ ...baseField }]], + [ + 'multiple fields', + [ + { ...baseField, id: 'test1' }, + { ...baseField, id: 'test2' }, + ], + [ + { ...baseField, id: 'test1' }, + { ...baseField, id: 'test2' }, + ], + ], + ])('returns false for identical lists: %s', (_scenario, oldFields, newFields) => { + expect(isResourceMapperFieldListStale(oldFields, newFields)).toBe(false); + }); + + // This test case is complex enough to keep separate + test('returns true when field is removed/replaced', () => { + const oldFields = [ + { ...baseField, id: 'test1' }, + { ...baseField, id: 'test2' }, + ]; + const newFields = [ + { ...baseField, id: 'test1' }, + { ...baseField, id: 'test3' }, // different id + ]; + expect(isResourceMapperFieldListStale(oldFields, newFields)).toBe(true); + }); +}); diff --git a/packages/editor-ui/src/utils/nodeTypesUtils.ts b/packages/editor-ui/src/utils/nodeTypesUtils.ts index 91fe812e4e..3b732dcda6 100644 --- a/packages/editor-ui/src/utils/nodeTypesUtils.ts +++ b/packages/editor-ui/src/utils/nodeTypesUtils.ts @@ -440,6 +440,42 @@ export const fieldCannotBeDeleted = ( ); }; +export const isResourceMapperFieldListStale = ( + oldFields: ResourceMapperField[], + newFields: ResourceMapperField[], +): boolean => { + if (oldFields.length !== newFields.length) { + return true; + } + + // Create map for O(1) lookup + const newFieldsMap = new Map(newFields.map((field) => [field.id, field])); + + // Check if any fields were removed or modified + for (const oldField of oldFields) { + const newField = newFieldsMap.get(oldField.id); + + // Field was removed + if (!newField) { + return true; + } + + // Check if any properties changed + if ( + oldField.displayName !== newField.displayName || + oldField.required !== newField.required || + oldField.defaultMatch !== newField.defaultMatch || + oldField.display !== newField.display || + oldField.canBeUsedToMatch !== newField.canBeUsedToMatch || + oldField.type !== newField.type + ) { + return true; + } + } + + return false; +}; + export const isMatchingField = ( field: string, matchingFields: string[], diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.json b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.json similarity index 100% rename from packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.json rename to packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.json diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts new file mode 100644 index 0000000000..f0352c9a31 --- /dev/null +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts @@ -0,0 +1,113 @@ +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow'; + +import { ExecuteWorkflow } from './ExecuteWorkflow.node'; +import { getWorkflowInfo } from './GenericFunctions'; + +jest.mock('./GenericFunctions'); +jest.mock('../../../utils/utilities'); + +describe('ExecuteWorkflow', () => { + const executeWorkflow = new ExecuteWorkflow(); + const executeFunctions = mock({ + getNodeParameter: jest.fn(), + getInputData: jest.fn(), + getWorkflowDataProxy: jest.fn(), + executeWorkflow: jest.fn(), + continueOnFail: jest.fn(), + setMetadata: jest.fn(), + getNode: jest.fn(), + }); + + beforeEach(() => { + jest.clearAllMocks(); + executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]); + executeFunctions.getWorkflowDataProxy.mockReturnValue({ + $workflow: { id: 'workflowId' }, + $execution: { id: 'executionId' }, + } as unknown as IWorkflowDataProxyData); + }); + + test('should execute workflow in "each" mode and wait for sub-workflow completion', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('each') // mode + .mockReturnValueOnce(true) // waitForSubWorkflow + .mockReturnValueOnce([]); // workflowInputs.schema + + executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]); + executeFunctions.getWorkflowDataProxy.mockReturnValue({ + $workflow: { id: 'workflowId' }, + $execution: { id: 'executionId' }, + } as unknown as IWorkflowDataProxyData); + (getWorkflowInfo as jest.Mock).mockResolvedValue({ id: 'subWorkflowId' }); + (executeFunctions.executeWorkflow as jest.Mock).mockResolvedValue({ + executionId: 'subExecutionId', + data: [[{ json: { key: 'subValue' } }]], + }); + + const result = await executeWorkflow.execute.call(executeFunctions); + + expect(result).toEqual([ + [ + { + json: { key: 'value' }, + index: 0, + pairedItem: { item: 0 }, + metadata: { + subExecution: { workflowId: 'subWorkflowId', executionId: 'subExecutionId' }, + }, + }, + ], + ]); + }); + + test('should execute workflow in "once" mode and not wait for sub-workflow completion', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('once') // mode + .mockReturnValueOnce(false) // waitForSubWorkflow + .mockReturnValueOnce([]); // workflowInputs.schema + + executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]); + + executeFunctions.executeWorkflow.mockResolvedValue({ + executionId: 'subExecutionId', + data: [[{ json: { key: 'subValue' } }]], + }); + + const result = await executeWorkflow.execute.call(executeFunctions); + + expect(result).toEqual([[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 } }]]); + }); + + test('should handle errors and continue on fail', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('each') // mode + .mockReturnValueOnce(true) // waitForSubWorkflow + .mockReturnValueOnce([]); // workflowInputs.schema + + (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); + (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true); + + const result = await executeWorkflow.execute.call(executeFunctions); + + expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]); + }); + + test('should throw error if not continuing on fail', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('each') // mode + .mockReturnValueOnce(true) // waitForSubWorkflow + .mockReturnValueOnce([]); // workflowInputs.schema + + (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); + (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(false); + + await expect(executeWorkflow.execute.call(executeFunctions)).rejects.toThrow( + 'Error executing workflow with item at index 0', + ); + }); +}); diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts similarity index 82% rename from packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts rename to packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts index 4dcc5cae64..a04ef4375c 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -8,8 +8,11 @@ import type { } from 'n8n-workflow'; import { getWorkflowInfo } from './GenericFunctions'; -import { generatePairedItemData } from '../../utils/utilities'; - +import { generatePairedItemData } from '../../../utils/utilities'; +import { + getCurrentWorkflowInputData, + loadWorkflowInputMappings, +} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; export class ExecuteWorkflow implements INodeType { description: INodeTypeDescription = { displayName: 'Execute Workflow', @@ -17,7 +20,7 @@ export class ExecuteWorkflow implements INodeType { icon: 'fa:sign-in-alt', iconColor: 'orange-red', group: ['transform'], - version: [1, 1.1], + version: [1, 1.1, 1.2], subtitle: '={{"Workflow: " + $parameter["workflowId"]}}', description: 'Execute another workflow', defaults: { @@ -40,6 +43,13 @@ export class ExecuteWorkflow implements INodeType { }, ], }, + { + displayName: 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + default: '', + }, { displayName: 'Source', name: 'source', @@ -68,6 +78,27 @@ export class ExecuteWorkflow implements INodeType { ], default: 'database', description: 'Where to get the workflow to execute from', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + }, + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + description: 'Load the workflow from the database by ID', + }, + { + name: 'Define Below', + value: 'parameter', + description: 'Pass the JSON code of a workflow', + }, + ], + default: 'database', + description: 'Where to get the workflow to execute from', + displayOptions: { show: { '@version': [{ _cnd: { gte: 1.2 } }] } }, }, // ---------------------------------- @@ -164,6 +195,43 @@ export class ExecuteWorkflow implements INodeType { name: 'executeWorkflowNotice', type: 'notice', default: '', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + }, + { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + type: 'resourceMapper', + noDataExpression: true, + default: { + mappingMode: 'defineBelow', + value: null, + }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['workflowId.value'], + resourceMapper: { + localResourceMapperMethod: 'loadWorkflowInputMappings', + valuesLabel: 'Workflow Inputs', + mode: 'map', + fieldWords: { + singular: 'input', + plural: 'inputs', + }, + addAllFields: true, + multiKeyMatch: false, + supportAutoMap: false, + showTypeConversionOptions: true, + }, + }, + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { gte: 1.2 } }], + }, + hide: { + workflowId: [''], + }, + }, }, { displayName: 'Mode', @@ -206,10 +274,16 @@ export class ExecuteWorkflow implements INodeType { ], }; + methods = { + localResourceMapping: { + loadWorkflowInputMappings, + }, + }; + async execute(this: IExecuteFunctions): Promise { const source = this.getNodeParameter('source', 0) as string; const mode = this.getNodeParameter('mode', 0, false) as string; - const items = this.getInputData(); + const items = getCurrentWorkflowInputData.call(this); const workflowProxy = this.getWorkflowDataProxy(0); const currentWorkflowId = workflowProxy.$workflow.id as string; diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/GenericFunctions.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/GenericFunctions.ts similarity index 92% rename from packages/nodes-base/nodes/ExecuteWorkflow/GenericFunctions.ts rename to packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/GenericFunctions.ts index 7588040bf8..450a268dfa 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/GenericFunctions.ts @@ -3,11 +3,16 @@ import { NodeOperationError, jsonParse } from 'n8n-workflow'; import type { IExecuteFunctions, IExecuteWorkflowInfo, + ILoadOptionsFunctions, INodeParameterResourceLocator, IRequestOptions, } from 'n8n-workflow'; -export async function getWorkflowInfo(this: IExecuteFunctions, source: string, itemIndex = 0) { +export async function getWorkflowInfo( + this: ILoadOptionsFunctions | IExecuteFunctions, + source: string, + itemIndex = 0, +) { const workflowInfo: IExecuteWorkflowInfo = {}; const nodeVersion = this.getNode().typeVersion; if (source === 'database') { diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.json b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.json similarity index 100% rename from packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.json rename to packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.json diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts new file mode 100644 index 0000000000..b479538c3a --- /dev/null +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts @@ -0,0 +1,53 @@ +import { mock } from 'jest-mock-extended'; +import type { FieldValueOption, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow'; + +import { ExecuteWorkflowTrigger } from './ExecuteWorkflowTrigger.node'; +import { WORKFLOW_INPUTS } from '../../../utils/workflowInputsResourceMapping/constants'; +import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; + +jest.mock('../../../utils/workflowInputsResourceMapping/GenericFunctions', () => ({ + getFieldEntries: jest.fn(), + getWorkflowInputData: jest.fn(), +})); + +describe('ExecuteWorkflowTrigger', () => { + const mockInputData: INodeExecutionData[] = [ + { json: { item: 0, foo: 'bar' }, index: 0 }, + { json: { item: 1, foo: 'quz' }, index: 1 }, + ]; + const mockNode = mock({ typeVersion: 1 }); + const executeFns = mock({ + getInputData: () => mockInputData, + getNode: () => mockNode, + getNodeParameter: jest.fn(), + }); + + it('should return its input data on V1 or V1.1 passthrough', async () => { + // User selection in V1.1, or fallback return value in V1 with dropdown not displayed + executeFns.getNodeParameter.mockReturnValueOnce('passthrough'); + const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); + + expect(result).toEqual([mockInputData]); + }); + + it('should filter out parent input in `Using Fields below` mode', async () => { + executeFns.getNodeParameter.mockReturnValueOnce(WORKFLOW_INPUTS); + const mockNewParams = [ + { name: 'value1', type: 'string' }, + { name: 'value2', type: 'number' }, + { name: 'foo', type: 'string' }, + ] as FieldValueOption[]; + const getFieldEntriesMock = (getFieldEntries as jest.Mock).mockReturnValue(mockNewParams); + + const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); + const expected = [ + [ + { index: 0, json: { value1: null, value2: null, foo: mockInputData[0].json.foo } }, + { index: 1, json: { value1: null, value2: null, foo: mockInputData[1].json.foo } }, + ], + ]; + + expect(result).toEqual(expected); + expect(getFieldEntriesMock).toHaveBeenCalledWith(executeFns); + }); +}); diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts new file mode 100644 index 0000000000..a15780a80e --- /dev/null +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts @@ -0,0 +1,225 @@ +import _ from 'lodash'; +import { + type INodeExecutionData, + NodeConnectionType, + type IExecuteFunctions, + type INodeType, + type INodeTypeDescription, +} from 'n8n-workflow'; + +import { + INPUT_SOURCE, + WORKFLOW_INPUTS, + JSON_EXAMPLE, + VALUES, + TYPE_OPTIONS, + PASSTHROUGH, + FALLBACK_DEFAULT_VALUE, +} from '../../../utils/workflowInputsResourceMapping/constants'; +import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; + +export class ExecuteWorkflowTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Execute Workflow Trigger', + name: 'executeWorkflowTrigger', + icon: 'fa:sign-out-alt', + group: ['trigger'], + version: [1, 1.1], + description: + 'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.', + eventTriggerDescription: '', + maxNodes: 1, + defaults: { + name: 'Workflow Input Trigger', + color: '#ff6d5a', + }, + inputs: [], + outputs: [NodeConnectionType.Main], + hints: [ + { + message: 'Please make sure to define your input fields.', + // This condition checks if we have no input fields, which gets a bit awkward: + // For WORKFLOW_INPUTS: keys() only contains `VALUES` if at least one value is provided + // For JSON_EXAMPLE: We remove all whitespace and check if we're left with an empty object. Note that we already error if the example is not valid JSON + displayCondition: + `={{$parameter['${INPUT_SOURCE}'] === '${WORKFLOW_INPUTS}' && !$parameter['${WORKFLOW_INPUTS}'].keys().length ` + + `|| $parameter['${INPUT_SOURCE}'] === '${JSON_EXAMPLE}' && $parameter['${JSON_EXAMPLE}'].toString().replaceAll(' ', '').replaceAll('\\n', '') === '{}' }}`, + whenToDisplay: 'always', + location: 'ndv', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'hidden', + noDataExpression: true, + options: [ + { + name: 'Workflow Call', + value: 'worklfow_call', + description: 'When called by another workflow using Execute Workflow Trigger', + action: 'When Called by Another Workflow', + }, + ], + default: 'worklfow_call', + }, + { + displayName: + "When an ‘execute workflow’ node calls this workflow, the execution starts here. Any data passed into the 'execute workflow' node will be output by this node.", + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { '@version': [{ _cnd: { eq: 1 } }] }, + }, + }, + { + displayName: 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { eq: 1 } }] } }, + default: '', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Input data mode', + name: INPUT_SOURCE, + type: 'options', + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Define using fields below', + value: WORKFLOW_INPUTS, + description: 'Provide input fields via UI', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Define using JSON example', + value: JSON_EXAMPLE, + description: 'Generate a schema from an example JSON object', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Accept all data', + value: PASSTHROUGH, + description: 'Use all incoming data from the parent workflow', + }, + ], + default: WORKFLOW_INPUTS, + noDataExpression: true, + displayOptions: { + show: { '@version': [{ _cnd: { gte: 1.1 } }] }, + }, + }, + { + displayName: + 'Provide an example object to infer fields and their types.
To allow any type for a given field, set the value to null.', + name: `${JSON_EXAMPLE}_notice`, + type: 'notice', + default: '', + displayOptions: { + show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] }, + }, + }, + { + displayName: 'JSON Example', + name: JSON_EXAMPLE, + type: 'json', + default: JSON.stringify( + { + aField: 'a string', + aNumber: 123, + thisFieldAcceptsAnyType: null, + anArray: [], + }, + null, + 2, + ), + noDataExpression: true, + displayOptions: { + show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] }, + }, + }, + { + displayName: 'Workflow Inputs', + name: WORKFLOW_INPUTS, + placeholder: 'Add field', + type: 'fixedCollection', + description: + 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', + typeOptions: { + multipleValues: true, + sortable: true, + minRequiredFields: 1, + }, + displayOptions: { + show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [WORKFLOW_INPUTS] }, + }, + default: {}, + options: [ + { + name: VALUES, + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + required: true, + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: TYPE_OPTIONS, + required: true, + default: 'string', + noDataExpression: true, + }, + ], + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions) { + const inputData = this.getInputData(); + const inputSource = this.getNodeParameter(INPUT_SOURCE, 0, PASSTHROUGH) as string; + + // Note on the data we receive from ExecuteWorkflow caller: + // + // The ExecuteWorkflow node typechecks all fields explicitly provided by the user here via the resourceMapper + // and removes all fields that are in the schema, but `removed` in the resourceMapper. + // + // In passthrough and legacy node versions, inputData will line up since the resourceMapper is empty, + // in which case all input is passed through. + // In other cases we will already have matching types and fields provided by the resource mapper, + // so we just need to be permissive on this end, + // while ensuring we provide default values for fields in our schema, which are removed in the resourceMapper. + + if (inputSource === PASSTHROUGH) { + return [inputData]; + } else { + const newParams = getFieldEntries(this); + const newKeys = new Set(newParams.map((x) => x.name)); + const itemsInSchema: INodeExecutionData[] = inputData.map((row, index) => ({ + json: { + ...Object.fromEntries(newParams.map((x) => [x.name, FALLBACK_DEFAULT_VALUE])), + // Need to trim to the expected schema to support legacy Execute Workflow callers passing through all their data + // which we do not want to expose past this node. + ..._.pickBy(row.json, (_value, key) => newKeys.has(key)), + }, + index, + })); + + return [itemsInSchema]; + } + } +} diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts deleted file mode 100644 index feb33a160d..0000000000 --- a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - NodeConnectionType, - type IExecuteFunctions, - type INodeType, - type INodeTypeDescription, -} from 'n8n-workflow'; - -export class ExecuteWorkflowTrigger implements INodeType { - description: INodeTypeDescription = { - displayName: 'Execute Workflow Trigger', - name: 'executeWorkflowTrigger', - icon: 'fa:sign-out-alt', - group: ['trigger'], - version: 1, - description: - 'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.', - eventTriggerDescription: '', - maxNodes: 1, - defaults: { - name: 'Execute Workflow Trigger', - color: '#ff6d5a', - }, - - inputs: [], - outputs: [NodeConnectionType.Main], - properties: [ - { - displayName: - "When an ‘execute workflow’ node calls this workflow, the execution starts here. Any data passed into the 'execute workflow' node will be output by this node.", - name: 'notice', - type: 'notice', - default: '', - }, - { - displayName: 'Events', - name: 'events', - type: 'hidden', - noDataExpression: true, - options: [ - { - name: 'Workflow Call', - value: 'worklfow_call', - description: 'When called by another workflow using Execute Workflow Trigger', - action: 'When Called by Another Workflow', - }, - ], - default: 'worklfow_call', - }, - ], - }; - - async execute(this: IExecuteFunctions) { - return [this.getInputData()]; - } -} diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts deleted file mode 100644 index ad35bff192..0000000000 --- a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { mock } from 'jest-mock-extended'; -import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; - -import { ExecuteWorkflowTrigger } from '../ExecuteWorkflowTrigger.node'; - -describe('ExecuteWorkflowTrigger', () => { - it('should return its input data', async () => { - const mockInputData: INodeExecutionData[] = [ - { json: { item: 0, foo: 'bar' } }, - { json: { item: 1, foo: 'quz' } }, - ]; - const executeFns = mock({ - getInputData: () => mockInputData, - }); - const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); - - expect(result).toEqual([mockInputData]); - }); -}); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 11585374bf..0307b66721 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -493,8 +493,8 @@ "dist/nodes/ErrorTrigger/ErrorTrigger.node.js", "dist/nodes/Eventbrite/EventbriteTrigger.node.js", "dist/nodes/ExecuteCommand/ExecuteCommand.node.js", - "dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js", - "dist/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js", + "dist/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.js", + "dist/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js", "dist/nodes/ExecutionData/ExecutionData.node.js", "dist/nodes/Facebook/FacebookGraphApi.node.js", "dist/nodes/Facebook/FacebookTrigger.node.js", @@ -867,6 +867,7 @@ "fast-glob": "catalog:", "fflate": "0.7.4", "get-system-fonts": "2.0.2", + "generate-schema": "2.6.0", "gm": "1.25.0", "html-to-text": "9.0.5", "iconv-lite": "catalog:", diff --git a/packages/nodes-base/tsconfig.build.json b/packages/nodes-base/tsconfig.build.json index 3a26457c9c..d92417abdd 100644 --- a/packages/nodes-base/tsconfig.build.json +++ b/packages/nodes-base/tsconfig.build.json @@ -8,7 +8,8 @@ "credentials/**/*.ts", "nodes/**/*.ts", "nodes/**/*.json", - "credentials/translations/**/*.json" + "credentials/translations/**/*.json", + "types/**/*.ts" ], "exclude": ["nodes/**/*.test.ts", "credentials/**/*.test.ts", "test/**"] } diff --git a/packages/nodes-base/tsconfig.json b/packages/nodes-base/tsconfig.json index b5a7282ff9..2cbb72109a 100644 --- a/packages/nodes-base/tsconfig.json +++ b/packages/nodes-base/tsconfig.json @@ -10,7 +10,13 @@ "noImplicitReturns": false, "useUnknownInCatchVariables": false }, - "include": ["credentials/**/*.ts", "nodes/**/*.ts", "test/**/*.ts", "utils/**/*.ts"], + "include": [ + "credentials/**/*.ts", + "nodes/**/*.ts", + "test/**/*.ts", + "utils/**/*.ts", + "types/**/*.ts" + ], "references": [ { "path": "../@n8n/imap/tsconfig.build.json" }, { "path": "../workflow/tsconfig.build.json" }, diff --git a/packages/nodes-base/types/generate-schema.d.ts b/packages/nodes-base/types/generate-schema.d.ts new file mode 100644 index 0000000000..90e0e15b05 --- /dev/null +++ b/packages/nodes-base/types/generate-schema.d.ts @@ -0,0 +1,27 @@ +declare module 'generate-schema' { + export interface SchemaObject { + $schema: string; + title?: string; + type: string; + properties?: { + [key: string]: SchemaObject | SchemaArray | SchemaProperty; + }; + required?: string[]; + items?: SchemaObject | SchemaArray; + } + + export interface SchemaArray { + type: string; + items?: SchemaObject | SchemaArray | SchemaProperty; + oneOf?: Array; + required?: string[]; + } + + export interface SchemaProperty { + type: string | string[]; + format?: string; + } + + export function json(title: string, schema: SchemaObject): SchemaObject; + export function json(schema: SchemaObject): SchemaObject; +} diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/.readme b/packages/nodes-base/utils/workflowInputsResourceMapping/.readme new file mode 100644 index 0000000000..e5556cc0cc --- /dev/null +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/.readme @@ -0,0 +1,5 @@ +These files contain reusable logic for workflow inputs mapping used in these nodes: + + - n8n-nodes-base.executeWorkflow + - n8n-nodes-base.executeWorkflowTrigger + - @n8n/n8n-nodes-langchain.toolWorkflow diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts new file mode 100644 index 0000000000..ba1c17f315 --- /dev/null +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts @@ -0,0 +1,167 @@ +import { json as generateSchemaFromExample, type SchemaObject } from 'generate-schema'; +import type { JSONSchema7 } from 'json-schema'; +import _ from 'lodash'; +import type { + FieldValueOption, + FieldType, + IWorkflowNodeContext, + INodeExecutionData, + IDataObject, + ResourceMapperField, + ILocalLoadOptionsFunctions, + ResourceMapperFields, + ISupplyDataFunctions, +} from 'n8n-workflow'; +import { jsonParse, NodeOperationError, EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE } from 'n8n-workflow'; + +import { + JSON_EXAMPLE, + INPUT_SOURCE, + WORKFLOW_INPUTS, + VALUES, + TYPE_OPTIONS, + PASSTHROUGH, +} from './constants'; + +const SUPPORTED_TYPES = TYPE_OPTIONS.map((x) => x.value); + +function parseJsonSchema(schema: JSONSchema7): FieldValueOption[] | string { + if (!schema?.properties) { + return 'Invalid JSON schema. Missing key `properties` in schema'; + } + + if (typeof schema.properties !== 'object') { + return 'Invalid JSON schema. Key `properties` is not an object'; + } + + const result: FieldValueOption[] = []; + for (const [name, v] of Object.entries(schema.properties)) { + if (typeof v !== 'object') { + return `Invalid JSON schema. Value for property '${name}' is not an object`; + } + + const type = v?.type; + + if (type === 'null') { + result.push({ name, type: 'any' }); + } else if (Array.isArray(type)) { + // Schema allows an array of types, but we don't + return `Invalid JSON schema. Array of types for property '${name}' is not supported by n8n. Either provide a single type or use type 'any' to allow any type`; + } else if (typeof type !== 'string') { + return `Invalid JSON schema. Unexpected non-string type ${type} for property '${name}'`; + } else if (!SUPPORTED_TYPES.includes(type as never)) { + return `Invalid JSON schema. Unsupported type ${type} for property '${name}'. Supported types are ${JSON.stringify(SUPPORTED_TYPES, null, 1)}`; + } else { + result.push({ name, type: type as FieldType }); + } + } + return result; +} + +function parseJsonExample(context: IWorkflowNodeContext): JSONSchema7 { + const jsonString = context.getNodeParameter(JSON_EXAMPLE, 0, '') as string; + const json = jsonParse(jsonString); + + return generateSchemaFromExample(json) as JSONSchema7; +} + +export function getFieldEntries(context: IWorkflowNodeContext): FieldValueOption[] { + const inputSource = context.getNodeParameter(INPUT_SOURCE, 0); + let result: FieldValueOption[] | string = 'Internal Error: Invalid input source'; + try { + if (inputSource === WORKFLOW_INPUTS) { + result = context.getNodeParameter( + `${WORKFLOW_INPUTS}.${VALUES}`, + 0, + [], + ) as FieldValueOption[]; + } else if (inputSource === JSON_EXAMPLE) { + const schema = parseJsonExample(context); + result = parseJsonSchema(schema); + } else if (inputSource === PASSTHROUGH) { + result = []; + } + } catch (e: unknown) { + result = + e && typeof e === 'object' && 'message' in e && typeof e.message === 'string' + ? e.message + : `Unknown error occurred: ${JSON.stringify(e)}`; + } + + if (Array.isArray(result)) { + return result; + } + throw new NodeOperationError(context.getNode(), result); +} + +export function getWorkflowInputValues(this: ISupplyDataFunctions): INodeExecutionData[] { + const inputData = this.getInputData(); + + return inputData.map((item, itemIndex) => { + const itemFieldValues = this.getNodeParameter( + 'workflowInputs.value', + itemIndex, + {}, + ) as IDataObject; + + return { + json: { + ...item.json, + ...itemFieldValues, + }, + index: itemIndex, + pairedItem: { + item: itemIndex, + }, + }; + }); +} + +export function getCurrentWorkflowInputData(this: ISupplyDataFunctions) { + const inputData: INodeExecutionData[] = getWorkflowInputValues.call(this); + + const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; + + if (schema.length === 0) { + return inputData; + } else { + const removedKeys = new Set(schema.filter((x) => x.removed).map((x) => x.displayName)); + + const filteredInputData: INodeExecutionData[] = inputData.map((item, index) => ({ + index, + pairedItem: { item: index }, + json: _.pickBy(item.json, (_v, key) => !removedKeys.has(key)), + })); + + return filteredInputData; + } +} + +export async function loadWorkflowInputMappings( + this: ILocalLoadOptionsFunctions, +): Promise { + const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); + let fields: ResourceMapperField[] = []; + + if (nodeLoadContext) { + const fieldValues = getFieldEntries(nodeLoadContext); + + fields = fieldValues.map((currentWorkflowInput) => { + const field: ResourceMapperField = { + id: currentWorkflowInput.name, + displayName: currentWorkflowInput.name, + required: false, + defaultMatch: false, + display: true, + canBeUsedToMatch: true, + }; + + if (currentWorkflowInput.type !== 'any') { + field.type = currentWorkflowInput.type; + } + + return field; + }); + } + return { fields }; +} diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts new file mode 100644 index 0000000000..409d8d703e --- /dev/null +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts @@ -0,0 +1,36 @@ +import type { FieldType } from 'n8n-workflow'; + +export const INPUT_SOURCE = 'inputSource'; +export const WORKFLOW_INPUTS = 'workflowInputs'; +export const VALUES = 'values'; +export const JSON_EXAMPLE = 'jsonExample'; +export const PASSTHROUGH = 'passthrough'; +export const TYPE_OPTIONS: Array<{ name: string; value: FieldType | 'any' }> = [ + { + name: 'Allow Any Type', + value: 'any', + }, + { + name: 'String', + value: 'string', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Array', + value: 'array', + }, + { + name: 'Object', + value: 'object', + }, + // Intentional omission of `dateTime`, `time`, `string-alphanumeric`, `form-fields`, `jwt` and `url` +]; + +export const FALLBACK_DEFAULT_VALUE = null; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index c5df420e29..6c28d4664d 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1017,9 +1017,23 @@ export interface ILoadOptionsFunctions extends FunctionsBase { options?: IGetNodeParameterOptions, ): NodeParameterValueType | object | undefined; getCurrentNodeParameters(): INodeParameters | undefined; + helpers: RequestHelperFunctions & SSHTunnelFunctions; } +export type FieldValueOption = { name: string; type: FieldType | 'any' }; + +export type IWorkflowNodeContext = ExecuteFunctions.GetNodeParameterFn & + Pick; + +export interface ILocalLoadOptionsFunctions { + getWorkflowNodeContext(nodeType: string): Promise; +} + +export interface IWorkflowLoader { + get(workflowId: string): Promise; +} + export interface IPollFunctions extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> { __emit( @@ -1293,14 +1307,18 @@ export interface INodePropertyTypeOptions { resourceMapper?: ResourceMapperTypeOptions; filter?: FilterTypeOptions; assignment?: AssignmentTypeOptions; + minRequiredFields?: number; // Supported by: fixedCollection + maxAllowedFields?: number; // Supported by: fixedCollection [key: string]: any; } -export interface ResourceMapperTypeOptions { - resourceMapperMethod: string; - mode: 'add' | 'update' | 'upsert'; +export interface ResourceMapperTypeOptionsBase { + mode: 'add' | 'update' | 'upsert' | 'map'; valuesLabel?: string; - fieldWords?: { singular: string; plural: string }; + fieldWords?: { + singular: string; + plural: string; + }; addAllFields?: boolean; noFieldsError?: string; multiKeyMatch?: boolean; @@ -1310,8 +1328,23 @@ export interface ResourceMapperTypeOptions { description?: string; hint?: string; }; + showTypeConversionOptions?: boolean; } +// Enforce at least one of resourceMapperMethod or localResourceMapperMethod +export type ResourceMapperTypeOptionsLocal = { + resourceMapperMethod: string; + localResourceMapperMethod?: never; // Explicitly disallows this property +}; + +export type ResourceMapperTypeOptionsExternal = { + localResourceMapperMethod: string; + resourceMapperMethod?: never; // Explicitly disallows this property +}; + +export type ResourceMapperTypeOptions = ResourceMapperTypeOptionsBase & + (ResourceMapperTypeOptionsLocal | ResourceMapperTypeOptionsExternal); + type NonEmptyArray = [T, ...T[]]; export type FilterTypeCombinator = 'and' | 'or'; @@ -1583,6 +1616,9 @@ export interface INodeType { resourceMapping?: { [functionName: string]: (this: ILoadOptionsFunctions) => Promise; }; + localResourceMapping?: { + [functionName: string]: (this: ILocalLoadOptionsFunctions) => Promise; + }; actionHandler?: { [functionName: string]: ( this: ILoadOptionsFunctions, @@ -2651,6 +2687,9 @@ export type ResourceMapperValue = { value: { [key: string]: string | number | boolean | null } | null; matchingColumns: string[]; schema: ResourceMapperField[]; + ignoreTypeMismatchErrors: boolean; + attemptToConvertTypes: boolean; + convertFieldsToString: boolean; }; export type FilterOperatorType = diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 114142bc34..22883a73bb 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -1568,7 +1568,7 @@ export function getParameterIssues( data: option as INodeProperties, }); } - } else if (nodeProperties.type === 'fixedCollection') { + } else if (nodeProperties.type === 'fixedCollection' && isDisplayed) { basePath = basePath ? `${basePath}.` : `${nodeProperties.name}.`; let propertyOptions: INodePropertyCollection; @@ -1579,6 +1579,24 @@ export function getParameterIssues( propertyOptions.name, basePath.slice(0, -1), ); + + // Validate allowed field counts + const valueArray = Array.isArray(value) ? value : []; + const { minRequiredFields, maxAllowedFields } = nodeProperties.typeOptions ?? {}; + let error = ''; + + if (minRequiredFields && valueArray.length < minRequiredFields) { + error = `At least ${minRequiredFields} ${minRequiredFields === 1 ? 'field is' : 'fields are'} required.`; + } + if (maxAllowedFields && valueArray.length > maxAllowedFields) { + error = `At most ${maxAllowedFields} ${maxAllowedFields === 1 ? 'field is' : 'fields are'} allowed.`; + } + if (error) { + foundIssues.parameters ??= {}; + foundIssues.parameters[nodeProperties.name] ??= []; + foundIssues.parameters[nodeProperties.name].push(error); + } + if (value === undefined) { continue; } diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index d94d9220d0..ef314cee18 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -974,7 +974,12 @@ export class WorkflowDataProxy { type: 'no_execution_data', }); } - return placeholdersDataInputData?.[name] ?? defaultValue; + return ( + // TS does not know that the key exists, we need to address this in refactor + (placeholdersDataInputData?.query as Record)?.[name] ?? + placeholdersDataInputData?.[name] ?? + defaultValue + ); }; const base = { diff --git a/packages/workflow/test/NodeHelpers.test.ts b/packages/workflow/test/NodeHelpers.test.ts index e56a4e20ac..8b3d9b3b02 100644 --- a/packages/workflow/test/NodeHelpers.test.ts +++ b/packages/workflow/test/NodeHelpers.test.ts @@ -1,5 +1,6 @@ import { NodeConnectionType, + type INodeIssues, type INode, type INodeParameters, type INodeProperties, @@ -11,6 +12,7 @@ import { getNodeHints, isSubNodeType, applyDeclarativeNodeOptionParameters, + getParameterIssues, } from '@/NodeHelpers'; import type { Workflow } from '@/Workflow'; @@ -3607,4 +3609,590 @@ describe('NodeHelpers', () => { expect(nodeType.description.properties).toEqual([]); }); }); + + describe('getParameterIssues', () => { + const tests: Array<{ + description: string; + input: { + nodeProperties: INodeProperties; + nodeValues: INodeParameters; + path: string; + node: INode; + }; + output: INodeIssues; + }> = [ + { + description: + 'Fixed collection::Should not return issues if minimum or maximum field count is not set', + input: { + nodeProperties: { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + placeholder: 'Add Field', + type: 'fixedCollection', + description: + 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', + typeOptions: { + multipleValues: true, + sortable: true, + }, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 1.1, + }, + }, + ], + inputSource: ['workflowInputs'], + }, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { + name: 'Allow Any Type', + value: 'any', + }, + { + name: 'String', + value: 'string', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Array', + value: 'array', + }, + { + name: 'Object', + value: 'object', + }, + ], + default: 'string', + noDataExpression: true, + }, + ], + }, + ], + }, + nodeValues: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + path: '', + node: { + parameters: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + type: 'n8n-nodes-base.executeWorkflowTrigger', + typeVersion: 1.1, + position: [-140, -20], + id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', + name: 'Test Node', + } as INode, + }, + output: {}, + }, + { + description: + 'Fixed collection::Should not return issues if field count is within the specified range', + input: { + nodeProperties: { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + placeholder: 'Add Field', + type: 'fixedCollection', + description: + 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', + typeOptions: { + multipleValues: true, + sortable: true, + minRequiredFields: 1, + maxAllowedFields: 3, + }, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 1.1, + }, + }, + ], + inputSource: ['workflowInputs'], + }, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { + name: 'Allow Any Type', + value: 'any', + }, + { + name: 'String', + value: 'string', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Array', + value: 'array', + }, + { + name: 'Object', + value: 'object', + }, + ], + default: 'string', + noDataExpression: true, + }, + ], + }, + ], + }, + nodeValues: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: { + values: [ + { + name: 'field1', + type: 'string', + }, + { + name: 'field2', + type: 'string', + }, + ], + }, + inputOptions: {}, + }, + path: '', + node: { + parameters: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + type: 'n8n-nodes-base.executeWorkflowTrigger', + typeVersion: 1.1, + position: [-140, -20], + id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', + name: 'Test Node', + } as INode, + }, + output: {}, + }, + { + description: + 'Fixed collection::Should return an issue if field count is lower than minimum specified', + input: { + nodeProperties: { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + placeholder: 'Add Field', + type: 'fixedCollection', + description: + 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', + typeOptions: { + multipleValues: true, + sortable: true, + minRequiredFields: 1, + }, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 1.1, + }, + }, + ], + inputSource: ['workflowInputs'], + }, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { + name: 'Allow Any Type', + value: 'any', + }, + { + name: 'String', + value: 'string', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Array', + value: 'array', + }, + { + name: 'Object', + value: 'object', + }, + ], + default: 'string', + noDataExpression: true, + }, + ], + }, + ], + }, + nodeValues: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + path: '', + node: { + parameters: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + type: 'n8n-nodes-base.executeWorkflowTrigger', + typeVersion: 1.1, + position: [-140, -20], + id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', + name: 'Test Node', + } as INode, + }, + output: { + parameters: { + workflowInputs: ['At least 1 field is required.'], + }, + }, + }, + { + description: + 'Fixed collection::Should return an issue if field count is higher than maximum specified', + input: { + nodeProperties: { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + placeholder: 'Add Field', + type: 'fixedCollection', + description: + 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', + typeOptions: { + multipleValues: true, + sortable: true, + maxAllowedFields: 1, + }, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 1.1, + }, + }, + ], + inputSource: ['workflowInputs'], + }, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { + name: 'Allow Any Type', + value: 'any', + }, + { + name: 'String', + value: 'string', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Array', + value: 'array', + }, + { + name: 'Object', + value: 'object', + }, + ], + default: 'string', + noDataExpression: true, + }, + ], + }, + ], + }, + nodeValues: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: { + values: [ + { + name: 'field1', + type: 'string', + }, + { + name: 'field2', + type: 'string', + }, + ], + }, + inputOptions: {}, + }, + path: '', + node: { + parameters: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + type: 'n8n-nodes-base.executeWorkflowTrigger', + typeVersion: 1.1, + position: [-140, -20], + id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', + name: 'Test Node', + } as INode, + }, + output: { + parameters: { + workflowInputs: ['At most 1 field is allowed.'], + }, + }, + }, + { + description: 'Fixed collection::Should not return issues if the collection is hidden', + input: { + nodeProperties: { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + placeholder: 'Add Field', + type: 'fixedCollection', + description: + 'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.', + typeOptions: { + multipleValues: true, + sortable: true, + maxAllowedFields: 1, + }, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 1.1, + }, + }, + ], + inputSource: ['workflowInputs'], + }, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + noDataExpression: true, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { + name: 'Allow Any Type', + value: 'any', + }, + { + name: 'String', + value: 'string', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Array', + value: 'array', + }, + { + name: 'Object', + value: 'object', + }, + ], + default: 'string', + noDataExpression: true, + }, + ], + }, + ], + }, + nodeValues: { + events: 'worklfow_call', + inputSource: 'somethingElse', + workflowInputs: { + values: [ + { + name: 'field1', + type: 'string', + }, + { + name: 'field2', + type: 'string', + }, + ], + }, + inputOptions: {}, + }, + path: '', + node: { + parameters: { + events: 'worklfow_call', + inputSource: 'workflowInputs', + workflowInputs: {}, + inputOptions: {}, + }, + type: 'n8n-nodes-base.executeWorkflowTrigger', + typeVersion: 1.1, + position: [-140, -20], + id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00', + name: 'Test Node', + } as INode, + }, + output: {}, + }, + ]; + + for (const testData of tests) { + test(testData.description, () => { + const result = getParameterIssues( + testData.input.nodeProperties, + testData.input.nodeValues, + testData.input.path, + testData.input.node, + ); + expect(result).toEqual(testData.output); + }); + } + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb4e7c8da..a42e557826 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1698,6 +1698,9 @@ importers: fflate: specifier: 0.7.4 version: 0.7.4 + generate-schema: + specifier: 2.6.0 + version: 2.6.0 get-system-fonts: specifier: 2.0.2 version: 2.0.2 From 06b86af7356b3be0af146c49f9720b24157b9e61 Mon Sep 17 00:00:00 2001 From: Dana <152518854+dana-gill@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:35:23 +0100 Subject: [PATCH 05/66] fix(Postgres Node): Account for JSON expressions (#12012) Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> --- .../nodes/Postgres/test/v2/operations.test.ts | 94 +++++++++++++++++++ .../nodes/Postgres/test/v2/utils.test.ts | 10 ++ .../database/executeQuery.operation.ts | 41 ++++---- .../nodes/Postgres/v2/helpers/utils.ts | 18 ++++ 4 files changed, 140 insertions(+), 23 deletions(-) diff --git a/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts b/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts index 206e6bf74a..a65c2d8de9 100644 --- a/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts +++ b/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts @@ -14,6 +14,7 @@ import * as select from '../../v2/actions/database/select.operation'; import * as update from '../../v2/actions/database/update.operation'; import * as upsert from '../../v2/actions/database/upsert.operation'; import type { ColumnInfo, PgpDatabase, QueriesRunner } from '../../v2/helpers/interfaces'; +import * as utils from '../../v2/helpers/utils'; const runQueries: QueriesRunner = jest.fn(); @@ -360,6 +361,99 @@ describe('Test PostgresV2, executeQuery operation', () => { nodeOptions, ); }); + + it('should execute queries with multiple json key/value pairs', async () => { + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query: 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)', + options: { + queryReplacement: + '={{ JSON.stringify({id: "7",id2: "848da11d-e72e-44c5-yyyy-c6fb9f17d366"}) }}', + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + expect(async () => { + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + }).not.toThrow(); + }); + + it('should execute queries with single json key/value pair', async () => { + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query: 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)', + options: { + queryReplacement: '={{ {"id": "7"} }}', + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + expect(async () => { + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + }).not.toThrow(); + }); + + it('should not parse out expressions if there are valid JSON query parameters', async () => { + const query = 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)'; + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query, + options: { + queryReplacement: '={{ {"id": "7"} }}', + nodeVersion: 2.6, + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + jest.spyOn(utils, 'isJSON'); + jest.spyOn(utils, 'stringToArray'); + + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + + expect(utils.isJSON).toHaveBeenCalledTimes(1); + expect(utils.stringToArray).toHaveBeenCalledTimes(0); + }); + + it('should parse out expressions if is invalid JSON in query parameters', async () => { + const query = 'SELECT *\nFROM users\nWHERE username IN ($1, $2, $3)'; + const nodeParameters: IDataObject = { + operation: 'executeQuery', + query, + options: { + queryReplacement: '={{ JSON.stringify({"id": "7"}}) }}', + nodeVersion: 2.6, + }, + }; + const nodeOptions = nodeParameters.options as IDataObject; + + jest.spyOn(utils, 'isJSON'); + jest.spyOn(utils, 'stringToArray'); + + await executeQuery.execute.call( + createMockExecuteFunction(nodeParameters), + runQueries, + items, + nodeOptions, + ); + + expect(utils.isJSON).toHaveBeenCalledTimes(1); + expect(utils.stringToArray).toHaveBeenCalledTimes(1); + }); }); describe('Test PostgresV2, insert operation', () => { diff --git a/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts b/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts index b8526c0e6f..25840b33bd 100644 --- a/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts +++ b/packages/nodes-base/nodes/Postgres/test/v2/utils.test.ts @@ -13,6 +13,7 @@ import { replaceEmptyStringsByNulls, wrapData, convertArraysToPostgresFormat, + isJSON, } from '../../v2/helpers/utils'; const node: INode = { @@ -26,6 +27,15 @@ const node: INode = { }, }; +describe('Test PostgresV2, isJSON', () => { + it('should return true for valid JSON', () => { + expect(isJSON('{"key": "value"}')).toEqual(true); + }); + it('should return false for invalid JSON', () => { + expect(isJSON('{"key": "value"')).toEqual(false); + }); +}); + describe('Test PostgresV2, wrapData', () => { it('should wrap object in json', () => { const data = { diff --git a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts index 06854bf018..75e0c78587 100644 --- a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts @@ -3,7 +3,6 @@ import type { IExecuteFunctions, INodeExecutionData, INodeProperties, - NodeParameterValueType, } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; @@ -15,7 +14,7 @@ import type { QueriesRunner, QueryWithValues, } from '../../helpers/interfaces'; -import { replaceEmptyStringsByNulls } from '../../helpers/utils'; +import { isJSON, replaceEmptyStringsByNulls, stringToArray } from '../../helpers/utils'; import { optionsCollection } from '../common.descriptions'; const properties: INodeProperties[] = [ @@ -54,20 +53,19 @@ export async function execute( nodeOptions: PostgresNodeOptions, _db?: PgpDatabase, ): Promise { - items = replaceEmptyStringsByNulls(items, nodeOptions.replaceEmptyStrings as boolean); - - const queries: QueryWithValues[] = []; - - for (let i = 0; i < items.length; i++) { - let query = this.getNodeParameter('query', i) as string; + const queries: QueryWithValues[] = replaceEmptyStringsByNulls( + items, + nodeOptions.replaceEmptyStrings as boolean, + ).map((_, index) => { + let query = this.getNodeParameter('query', index) as string; for (const resolvable of getResolvables(query)) { - query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string); + query = query.replace(resolvable, this.evaluateExpression(resolvable, index) as string); } let values: Array = []; - let queryReplacement = this.getNodeParameter('options.queryReplacement', i, ''); + let queryReplacement = this.getNodeParameter('options.queryReplacement', index, ''); if (typeof queryReplacement === 'number') { queryReplacement = String(queryReplacement); @@ -78,14 +76,6 @@ export async function execute( const rawReplacements = (node.parameters.options as IDataObject)?.queryReplacement as string; - const stringToArray = (str: NodeParameterValueType | undefined) => { - if (str === undefined) return []; - return String(str) - .split(',') - .filter((entry) => entry) - .map((entry) => entry.trim()); - }; - if (rawReplacements) { const nodeVersion = nodeOptions.nodeVersion as number; @@ -94,7 +84,12 @@ export async function execute( const resolvables = getResolvables(rawValues); if (resolvables.length) { for (const resolvable of resolvables) { - const evaluatedValues = stringToArray(this.evaluateExpression(`${resolvable}`, i)); + const evaluatedExpression = + this.evaluateExpression(`${resolvable}`, index)?.toString() ?? ''; + const evaluatedValues = isJSON(evaluatedExpression) + ? [evaluatedExpression] + : stringToArray(evaluatedExpression); + if (evaluatedValues.length) values.push(...evaluatedValues); } } else { @@ -112,7 +107,7 @@ export async function execute( if (resolvables.length) { for (const resolvable of resolvables) { - values.push(this.evaluateExpression(`${resolvable}`, i) as IDataObject); + values.push(this.evaluateExpression(`${resolvable}`, index) as IDataObject); } } else { values.push(rawValue); @@ -127,7 +122,7 @@ export async function execute( throw new NodeOperationError( this.getNode(), 'Query Parameters must be a string of comma-separated values or an array of values', - { itemIndex: i }, + { itemIndex: index }, ); } } @@ -142,8 +137,8 @@ export async function execute( } } - queries.push({ query, values, options: { partial: true } }); - } + return { query, values, options: { partial: true } }; + }); return await runQueries(queries, items, nodeOptions); } diff --git a/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts b/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts index 58f1c8a96f..69e0ff9046 100644 --- a/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts @@ -4,6 +4,7 @@ import type { INode, INodeExecutionData, INodePropertyOptions, + NodeParameterValueType, } from 'n8n-workflow'; import { NodeOperationError, jsonParse } from 'n8n-workflow'; @@ -20,6 +21,23 @@ import type { } from './interfaces'; import { generatePairedItemData } from '../../../../utils/utilities'; +export function isJSON(str: string) { + try { + JSON.parse(str.trim()); + return true; + } catch { + return false; + } +} + +export function stringToArray(str: NodeParameterValueType | undefined) { + if (str === undefined) return []; + return String(str) + .split(',') + .filter((entry) => entry) + .map((entry) => entry.trim()); +} + export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] { if (!Array.isArray(data)) { return [{ json: data }]; From 724e08562f1fc4ab58bfbc0212f297b87dd95465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 20 Dec 2024 18:41:05 +0100 Subject: [PATCH 06/66] refactor(core): Deduplicate isObjectLiteral, add docs and tests (#12332) --- .../common/1659888469333-AddJsonKeyPinData.ts | 2 +- packages/cli/src/logging/logger.service.ts | 3 +- .../list-query/dtos/base.filter.dto.ts | 3 +- .../services/credentials-tester.service.ts | 3 +- packages/cli/src/utils.ts | 4 --- .../src/workflow-execute-additional-data.ts | 4 +-- packages/core/src/SerializedBuffer.ts | 6 ++-- packages/core/src/__tests__/utils.test.ts | 35 +++++++++++++++++++ packages/core/src/index.ts | 1 + packages/core/src/utils.ts | 18 ++++++++++ 10 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 packages/core/src/__tests__/utils.test.ts create mode 100644 packages/core/src/utils.ts diff --git a/packages/cli/src/databases/migrations/common/1659888469333-AddJsonKeyPinData.ts b/packages/cli/src/databases/migrations/common/1659888469333-AddJsonKeyPinData.ts index 84d3040a10..75ce65ad6b 100644 --- a/packages/cli/src/databases/migrations/common/1659888469333-AddJsonKeyPinData.ts +++ b/packages/cli/src/databases/migrations/common/1659888469333-AddJsonKeyPinData.ts @@ -1,7 +1,7 @@ +import { isObjectLiteral } from 'n8n-core'; import type { IDataObject, INodeExecutionData } from 'n8n-workflow'; import type { MigrationContext, IrreversibleMigration } from '@/databases/types'; -import { isObjectLiteral } from '@/utils'; type OldPinnedData = { [nodeName: string]: IDataObject[] }; type NewPinnedData = { [nodeName: string]: INodeExecutionData[] }; diff --git a/packages/cli/src/logging/logger.service.ts b/packages/cli/src/logging/logger.service.ts index 46471c0611..46441e5a33 100644 --- a/packages/cli/src/logging/logger.service.ts +++ b/packages/cli/src/logging/logger.service.ts @@ -2,7 +2,7 @@ import type { LogScope } from '@n8n/config'; import { GlobalConfig } from '@n8n/config'; import callsites from 'callsites'; import type { TransformableInfo } from 'logform'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, isObjectLiteral } from 'n8n-core'; import { LoggerProxy, LOG_LEVELS } from 'n8n-workflow'; import path, { basename } from 'node:path'; import pc from 'picocolors'; @@ -10,7 +10,6 @@ import { Service } from 'typedi'; import winston from 'winston'; import { inDevelopment, inProduction } from '@/constants'; -import { isObjectLiteral } from '@/utils'; import { noOp } from './constants'; import type { LogLocationMetadata, LogLevel, LogMetadata } from './types'; diff --git a/packages/cli/src/middlewares/list-query/dtos/base.filter.dto.ts b/packages/cli/src/middlewares/list-query/dtos/base.filter.dto.ts index 678cb86981..9fa5c21677 100644 --- a/packages/cli/src/middlewares/list-query/dtos/base.filter.dto.ts +++ b/packages/cli/src/middlewares/list-query/dtos/base.filter.dto.ts @@ -1,9 +1,8 @@ import { plainToInstance, instanceToPlain } from 'class-transformer'; import { validate } from 'class-validator'; +import { isObjectLiteral } from 'n8n-core'; import { ApplicationError, jsonParse } from 'n8n-workflow'; -import { isObjectLiteral } from '@/utils'; - export class BaseFilter { protected static async toFilter(rawFilter: string, Filter: typeof BaseFilter) { const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' }); diff --git a/packages/cli/src/services/credentials-tester.service.ts b/packages/cli/src/services/credentials-tester.service.ts index 4a999d6541..6ae7201ac0 100644 --- a/packages/cli/src/services/credentials-tester.service.ts +++ b/packages/cli/src/services/credentials-tester.service.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ import get from 'lodash/get'; -import { ErrorReporter, NodeExecuteFunctions, RoutingNode } from 'n8n-core'; +import { ErrorReporter, NodeExecuteFunctions, RoutingNode, isObjectLiteral } from 'n8n-core'; import type { ICredentialsDecrypted, ICredentialTestFunction, @@ -34,7 +34,6 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da import { RESPONSE_ERROR_MESSAGES } from '../constants'; import { CredentialsHelper } from '../credentials-helper'; -import { isObjectLiteral } from '../utils'; const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES; diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 700f74f9d0..d701707a11 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -58,10 +58,6 @@ export function isStringArray(value: unknown): value is string[] { export const isIntegerString = (value: string) => /^\d+$/.test(value); -export function isObjectLiteral(item: unknown): item is { [key: string]: unknown } { - return typeof item === 'object' && item !== null && !Array.isArray(item); -} - export function removeTrailingSlash(path: string) { return path.endsWith('/') ? path.slice(0, -1) : path; } diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index a97bb3d3fa..d020b4d868 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -5,7 +5,7 @@ import type { PushType } from '@n8n/api-types'; import { GlobalConfig } from '@n8n/config'; import { stringify } from 'flatted'; -import { ErrorReporter, WorkflowExecute } from 'n8n-core'; +import { ErrorReporter, WorkflowExecute, isObjectLiteral } from 'n8n-core'; import { ApplicationError, NodeOperationError, Workflow, WorkflowHooks } from 'n8n-workflow'; import type { IDataObject, @@ -45,7 +45,7 @@ import type { IWorkflowErrorData, UpdateExecutionPayload } from '@/interfaces'; import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; -import { findSubworkflowStart, isObjectLiteral, isWorkflowIdValid } from '@/utils'; +import { findSubworkflowStart, isWorkflowIdValid } from '@/utils'; import * as WorkflowHelpers from '@/workflow-helpers'; import { WorkflowRepository } from './databases/repositories/workflow.repository'; diff --git a/packages/core/src/SerializedBuffer.ts b/packages/core/src/SerializedBuffer.ts index 48395049b9..7a96884729 100644 --- a/packages/core/src/SerializedBuffer.ts +++ b/packages/core/src/SerializedBuffer.ts @@ -1,3 +1,5 @@ +import { isObjectLiteral } from './utils'; + /** A nodejs Buffer gone through JSON.stringify */ export type SerializedBuffer = { type: 'Buffer'; @@ -9,10 +11,6 @@ export function toBuffer(serializedBuffer: SerializedBuffer): Buffer { return Buffer.from(serializedBuffer.data); } -function isObjectLiteral(item: unknown): item is { [key: string]: unknown } { - return typeof item === 'object' && item !== null && !Array.isArray(item); -} - export function isSerializedBuffer(candidate: unknown): candidate is SerializedBuffer { return ( isObjectLiteral(candidate) && diff --git a/packages/core/src/__tests__/utils.test.ts b/packages/core/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..a8532ed589 --- /dev/null +++ b/packages/core/src/__tests__/utils.test.ts @@ -0,0 +1,35 @@ +import { isObjectLiteral } from '@/utils'; + +describe('isObjectLiteral', () => { + test.each([ + ['empty object literal', {}, true], + ['object with properties', { foo: 'bar', num: 123 }, true], + ['nested object literal', { nested: { foo: 'bar' } }, true], + ['object with symbol key', { [Symbol.for('foo')]: 'bar' }, true], + ['null', null, false], + ['empty array', [], false], + ['array with values', [1, 2, 3], false], + ['number', 42, false], + ['string', 'string', false], + ['boolean', true, false], + ['undefined', undefined, false], + ['Date object', new Date(), false], + ['RegExp object', new RegExp(''), false], + ['Map object', new Map(), false], + ['Set object', new Set(), false], + ['arrow function', () => {}, false], + ['regular function', function () {}, false], + ['class instance', new (class TestClass {})(), false], + ['object with custom prototype', Object.create({ customMethod: () => {} }), true], + ['Object.create(null)', Object.create(null), false], + ['Buffer', Buffer.from('test'), false], + ['Serialized Buffer', Buffer.from('test').toJSON(), true], + ['Promise', new Promise(() => {}), false], + ])('should return %s for %s', (_, input, expected) => { + expect(isObjectLiteral(input)).toBe(expected); + }); + + it('should return false for Error objects', () => { + expect(isObjectLiteral(new Error())).toBe(false); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1fc9d77399..bec9767ffa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,3 +25,4 @@ export * from './node-execution-context'; export * from './PartialExecutionUtils'; export { ErrorReporter } from './error-reporter'; export * from './SerializedBuffer'; +export { isObjectLiteral } from './utils'; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts new file mode 100644 index 0000000000..c0d29c83dc --- /dev/null +++ b/packages/core/src/utils.ts @@ -0,0 +1,18 @@ +type ObjectLiteral = { [key: string | symbol]: unknown }; + +/** + * Checks if the provided value is a plain object literal (not null, not an array, not a class instance, and not a primitive). + * This function serves as a type guard. + * + * @param candidate - The value to check + * @returns {boolean} True if the value is an object literal, false otherwise + */ +export function isObjectLiteral(candidate: unknown): candidate is ObjectLiteral { + return ( + typeof candidate === 'object' && + candidate !== null && + !Array.isArray(candidate) && + // eslint-disable-next-line @typescript-eslint/ban-types + (Object.getPrototypeOf(candidate) as Object)?.constructor?.name === 'Object' + ); +} From fe7fb41ad89f1c36c414a4f103285b336c14ccd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 20 Dec 2024 19:45:04 +0100 Subject: [PATCH 07/66] refactor(core): Fix push message type inference (#12331) --- .../collaboration/collaboration.service.ts | 2 +- .../community-packages.controller.ts | 45 ++++++++---- .../cli/src/controllers/e2e.controller.ts | 13 ++-- .../cli/src/events/maps/pub-sub.event-map.ts | 6 +- .../executions/execution-recovery.service.ts | 2 +- .../cli/src/load-nodes-and-credentials.ts | 2 +- .../src/push/__tests__/websocket.push.test.ts | 6 +- packages/cli/src/push/abstract.push.ts | 20 +++--- packages/cli/src/push/index.ts | 20 +++--- .../scaling/__tests__/pubsub-handler.test.ts | 50 ++++++++++---- .../src/scaling/pubsub/publisher.service.ts | 4 +- .../cli/src/scaling/pubsub/pubsub-handler.ts | 28 +++++--- .../src/scaling/pubsub/subscriber.service.ts | 4 +- packages/cli/src/webhooks/test-webhooks.ts | 5 +- .../src/workflow-execute-additional-data.ts | 38 ++++++----- .../collaboration.service.test.ts | 68 ++++++++++--------- 16 files changed, 178 insertions(+), 135 deletions(-) diff --git a/packages/cli/src/collaboration/collaboration.service.ts b/packages/cli/src/collaboration/collaboration.service.ts index ece93bd5b2..a6e957f510 100644 --- a/packages/cli/src/collaboration/collaboration.service.ts +++ b/packages/cli/src/collaboration/collaboration.service.ts @@ -99,6 +99,6 @@ export class CollaborationService { collaborators: activeCollaborators, }; - this.push.sendToUsers('collaboratorsChanged', msgData, userIds); + this.push.sendToUsers({ type: 'collaboratorsChanged', data: msgData }, userIds); } } diff --git a/packages/cli/src/controllers/community-packages.controller.ts b/packages/cli/src/controllers/community-packages.controller.ts index 918f1cdf74..ab2134b7e0 100644 --- a/packages/cli/src/controllers/community-packages.controller.ts +++ b/packages/cli/src/controllers/community-packages.controller.ts @@ -115,9 +115,12 @@ export class CommunityPackagesController { // broadcast to connected frontends that node list has been updated installedPackage.installedNodes.forEach((node) => { - this.push.broadcast('reloadNodeType', { - name: node.type, - version: node.latestVersion, + this.push.broadcast({ + type: 'reloadNodeType', + data: { + name: node.type, + version: node.latestVersion, + }, }); }); @@ -206,9 +209,12 @@ export class CommunityPackagesController { // broadcast to connected frontends that node list has been updated installedPackage.installedNodes.forEach((node) => { - this.push.broadcast('removeNodeType', { - name: node.type, - version: node.latestVersion, + this.push.broadcast({ + type: 'removeNodeType', + data: { + name: node.type, + version: node.latestVersion, + }, }); }); @@ -246,16 +252,22 @@ export class CommunityPackagesController { // broadcast to connected frontends that node list has been updated previouslyInstalledPackage.installedNodes.forEach((node) => { - this.push.broadcast('removeNodeType', { - name: node.type, - version: node.latestVersion, + this.push.broadcast({ + type: 'removeNodeType', + data: { + name: node.type, + version: node.latestVersion, + }, }); }); newInstalledPackage.installedNodes.forEach((node) => { - this.push.broadcast('reloadNodeType', { - name: node.name, - version: node.latestVersion, + this.push.broadcast({ + type: 'reloadNodeType', + data: { + name: node.name, + version: node.latestVersion, + }, }); }); @@ -272,9 +284,12 @@ export class CommunityPackagesController { return newInstalledPackage; } catch (error) { previouslyInstalledPackage.installedNodes.forEach((node) => { - this.push.broadcast('removeNodeType', { - name: node.type, - version: node.latestVersion, + this.push.broadcast({ + type: 'removeNodeType', + data: { + name: node.type, + version: node.latestVersion, + }, }); }); diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 9c5a1ff36d..a61342320d 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -1,4 +1,4 @@ -import type { PushPayload, PushType } from '@n8n/api-types'; +import type { PushMessage } from '@n8n/api-types'; import { Request } from 'express'; import Container from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -58,14 +58,12 @@ type ResetRequest = Request< } >; -type PushRequest = Request< +type PushRequest = Request< {}, {}, { - type: T; pushRef: string; - data: PushPayload; - } + } & PushMessage >; @RestController('/e2e') @@ -144,8 +142,9 @@ export class E2EController { } @Post('/push', { skipAuth: true }) - async pushSend(req: PushRequest) { - this.push.broadcast(req.body.type, req.body.data); + async pushSend(req: PushRequest) { + const { pushRef: _, ...pushMsg } = req.body; + this.push.broadcast(pushMsg); } @Patch('/feature', { skipAuth: true }) diff --git a/packages/cli/src/events/maps/pub-sub.event-map.ts b/packages/cli/src/events/maps/pub-sub.event-map.ts index ff27741b9b..0d71fcff91 100644 --- a/packages/cli/src/events/maps/pub-sub.event-map.ts +++ b/packages/cli/src/events/maps/pub-sub.event-map.ts @@ -1,4 +1,4 @@ -import type { PushType, WorkerStatus } from '@n8n/api-types'; +import type { PushMessage, WorkerStatus } from '@n8n/api-types'; import type { IWorkflowDb } from '@/interfaces'; @@ -64,9 +64,7 @@ export type PubSubCommandMap = { errorMessage: string; }; - 'relay-execution-lifecycle-event': { - type: PushType; - args: Record; + 'relay-execution-lifecycle-event': PushMessage & { pushRef: string; }; diff --git a/packages/cli/src/executions/execution-recovery.service.ts b/packages/cli/src/executions/execution-recovery.service.ts index 33576d1368..a10fc995a4 100644 --- a/packages/cli/src/executions/execution-recovery.service.ts +++ b/packages/cli/src/executions/execution-recovery.service.ts @@ -49,7 +49,7 @@ export class ExecutionRecoveryService { this.push.once('editorUiConnected', async () => { await sleep(1000); - this.push.broadcast('executionRecovered', { executionId }); + this.push.broadcast({ type: 'executionRecovered', data: { executionId } }); }); return amendedExecution; diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index f6f66d024b..db62e8415e 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -520,7 +520,7 @@ export class LoadNodesAndCredentials { loader.reset(); await loader.loadAll(); await this.postProcessLoaders(); - push.broadcast('nodeDescriptionUpdated', {}); + push.broadcast({ type: 'nodeDescriptionUpdated', data: {} }); }, 100); const toWatch = loader.isLazyLoaded diff --git a/packages/cli/src/push/__tests__/websocket.push.test.ts b/packages/cli/src/push/__tests__/websocket.push.test.ts index 2362e5a0c6..fd1e2f27a0 100644 --- a/packages/cli/src/push/__tests__/websocket.push.test.ts +++ b/packages/cli/src/push/__tests__/websocket.push.test.ts @@ -73,7 +73,7 @@ describe('WebSocketPush', () => { it('sends data to one connection', () => { webSocketPush.add(pushRef1, userId, mockWebSocket1); webSocketPush.add(pushRef2, userId, mockWebSocket2); - webSocketPush.sendToOne(pushMessage.type, pushMessage.data, pushRef1); + webSocketPush.sendToOne(pushMessage, pushRef1); expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedMsg); expect(mockWebSocket2.send).not.toHaveBeenCalled(); @@ -82,7 +82,7 @@ describe('WebSocketPush', () => { it('sends data to all connections', () => { webSocketPush.add(pushRef1, userId, mockWebSocket1); webSocketPush.add(pushRef2, userId, mockWebSocket2); - webSocketPush.sendToAll(pushMessage.type, pushMessage.data); + webSocketPush.sendToAll(pushMessage); expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedMsg); expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedMsg); @@ -101,7 +101,7 @@ describe('WebSocketPush', () => { it('sends data to all users connections', () => { webSocketPush.add(pushRef1, userId, mockWebSocket1); webSocketPush.add(pushRef2, userId, mockWebSocket2); - webSocketPush.sendToUsers(pushMessage.type, pushMessage.data, [userId]); + webSocketPush.sendToUsers(pushMessage, [userId]); expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedMsg); expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedMsg); diff --git a/packages/cli/src/push/abstract.push.ts b/packages/cli/src/push/abstract.push.ts index 83a859fc75..574f8a0def 100644 --- a/packages/cli/src/push/abstract.push.ts +++ b/packages/cli/src/push/abstract.push.ts @@ -1,4 +1,4 @@ -import type { PushPayload, PushType } from '@n8n/api-types'; +import type { PushMessage } from '@n8n/api-types'; import { ErrorReporter } from 'n8n-core'; import { assert, jsonStringify } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -69,7 +69,7 @@ export abstract class AbstractPush extends TypedEmitter(type: Type, data: PushPayload, pushRefs: string[]) { + private sendTo({ type, data }: PushMessage, pushRefs: string[]) { this.logger.debug(`Pushed to frontend: ${type}`, { dataType: type, pushRefs: pushRefs.join(', '), @@ -90,30 +90,26 @@ export abstract class AbstractPush extends TypedEmitter(type: Type, data: PushPayload) { - this.sendTo(type, data, Object.keys(this.connections)); + sendToAll(pushMsg: PushMessage) { + this.sendTo(pushMsg, Object.keys(this.connections)); } - sendToOne(type: Type, data: PushPayload, pushRef: string) { + sendToOne(pushMsg: PushMessage, pushRef: string) { if (this.connections[pushRef] === undefined) { this.logger.error(`The session "${pushRef}" is not registered.`, { pushRef }); return; } - this.sendTo(type, data, [pushRef]); + this.sendTo(pushMsg, [pushRef]); } - sendToUsers( - type: Type, - data: PushPayload, - userIds: Array, - ) { + sendToUsers(pushMsg: PushMessage, userIds: Array) { const { connections } = this; const userPushRefs = Object.keys(connections).filter((pushRef) => userIds.includes(this.userIdByPushRef[pushRef]), ); - this.sendTo(type, data, userPushRefs); + this.sendTo(pushMsg, userPushRefs); } closeAllConnections() { diff --git a/packages/cli/src/push/index.ts b/packages/cli/src/push/index.ts index 0007001e33..7325981d0b 100644 --- a/packages/cli/src/push/index.ts +++ b/packages/cli/src/push/index.ts @@ -1,4 +1,4 @@ -import type { PushPayload, PushType } from '@n8n/api-types'; +import type { PushMessage } from '@n8n/api-types'; import type { Application } from 'express'; import { ServerResponse } from 'http'; import type { Server } from 'http'; @@ -81,11 +81,11 @@ export class Push extends TypedEmitter { this.emit('editorUiConnected', pushRef); } - broadcast(type: Type, data: PushPayload) { - this.backend.sendToAll(type, data); + broadcast(pushMsg: PushMessage) { + this.backend.sendToAll(pushMsg); } - send(type: Type, data: PushPayload, pushRef: string) { + send(pushMsg: PushMessage, pushRef: string) { /** * Multi-main setup: In a manual webhook execution, the main process that * handles a webhook might not be the same as the main process that created @@ -95,20 +95,16 @@ export class Push extends TypedEmitter { if (this.instanceSettings.isMultiMain && !this.backend.hasPushRef(pushRef)) { void this.publisher.publishCommand({ command: 'relay-execution-lifecycle-event', - payload: { type, args: data, pushRef }, + payload: { ...pushMsg, pushRef }, }); return; } - this.backend.sendToOne(type, data, pushRef); + this.backend.sendToOne(pushMsg, pushRef); } - sendToUsers( - type: Type, - data: PushPayload, - userIds: Array, - ) { - this.backend.sendToUsers(type, data, userIds); + sendToUsers(pushMsg: PushMessage, userIds: Array) { + this.backend.sendToUsers(pushMsg, userIds); } @OnShutdown() diff --git a/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts b/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts index 314ded0b8b..4f8c8af859 100644 --- a/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts +++ b/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts @@ -620,7 +620,10 @@ describe('PubSubHandler', () => { expect(activeWorkflowManager.add).toHaveBeenCalledWith(workflowId, 'activate', undefined, { shouldPublish: false, }); - expect(push.broadcast).toHaveBeenCalledWith('workflowActivated', { workflowId }); + expect(push.broadcast).toHaveBeenCalledWith({ + type: 'workflowActivated', + data: { workflowId }, + }); expect(publisher.publishCommand).toHaveBeenCalledWith({ command: 'display-workflow-activation', payload: { workflowId }, @@ -680,7 +683,10 @@ describe('PubSubHandler', () => { expect(activeWorkflowManager.removeWorkflowTriggersAndPollers).toHaveBeenCalledWith( workflowId, ); - expect(push.broadcast).toHaveBeenCalledWith('workflowDeactivated', { workflowId }); + expect(push.broadcast).toHaveBeenCalledWith({ + type: 'workflowDeactivated', + data: { workflowId }, + }); expect(publisher.publishCommand).toHaveBeenCalledWith({ command: 'display-workflow-deactivation', payload: { workflowId }, @@ -735,7 +741,10 @@ describe('PubSubHandler', () => { eventService.emit('display-workflow-activation', { workflowId }); - expect(push.broadcast).toHaveBeenCalledWith('workflowActivated', { workflowId }); + expect(push.broadcast).toHaveBeenCalledWith({ + type: 'workflowActivated', + data: { workflowId }, + }); }); it('should handle `display-workflow-deactivation` event', () => { @@ -758,7 +767,10 @@ describe('PubSubHandler', () => { eventService.emit('display-workflow-deactivation', { workflowId }); - expect(push.broadcast).toHaveBeenCalledWith('workflowDeactivated', { workflowId }); + expect(push.broadcast).toHaveBeenCalledWith({ + type: 'workflowDeactivated', + data: { workflowId }, + }); }); it('should handle `display-workflow-activation-error` event', () => { @@ -782,9 +794,12 @@ describe('PubSubHandler', () => { eventService.emit('display-workflow-activation-error', { workflowId, errorMessage }); - expect(push.broadcast).toHaveBeenCalledWith('workflowFailedToActivate', { - workflowId, - errorMessage, + expect(push.broadcast).toHaveBeenCalledWith({ + type: 'workflowFailedToActivate', + data: { + workflowId, + errorMessage, + }, }); }); @@ -806,15 +821,21 @@ describe('PubSubHandler', () => { const pushRef = 'test-push-ref'; const type = 'executionStarted'; - const args = { testArg: 'value' }; + const data = { + executionId: '123', + mode: 'webhook' as const, + startedAt: new Date(), + workflowId: '456', + flattedRunData: '[]', + }; push.getBackend.mockReturnValue( mock({ hasPushRef: jest.fn().mockReturnValue(true) }), ); - eventService.emit('relay-execution-lifecycle-event', { type, args, pushRef }); + eventService.emit('relay-execution-lifecycle-event', { type, data, pushRef }); - expect(push.send).toHaveBeenCalledWith(type, args, pushRef); + expect(push.send).toHaveBeenCalledWith({ type, data }, pushRef); }); it('should handle `clear-test-webhooks` event', () => { @@ -868,9 +889,12 @@ describe('PubSubHandler', () => { eventService.emit('response-to-get-worker-status', workerStatus); - expect(push.broadcast).toHaveBeenCalledWith('sendWorkerStatusMessage', { - workerId: workerStatus.senderId, - status: workerStatus, + expect(push.broadcast).toHaveBeenCalledWith({ + type: 'sendWorkerStatusMessage', + data: { + workerId: workerStatus.senderId, + status: workerStatus, + }, }); }); }); diff --git a/packages/cli/src/scaling/pubsub/publisher.service.ts b/packages/cli/src/scaling/pubsub/publisher.service.ts index fc007f76c0..4723b1d37d 100644 --- a/packages/cli/src/scaling/pubsub/publisher.service.ts +++ b/packages/cli/src/scaling/pubsub/publisher.service.ts @@ -65,10 +65,10 @@ export class Publisher { const metadata: LogMetadata = { msg: msg.command, channel: 'n8n.commands' }; if (msg.command === 'relay-execution-lifecycle-event') { - const { args, type } = msg.payload; + const { data, type } = msg.payload; msgName += ` (${type})`; metadata.type = type; - metadata.executionId = args.executionId; + if ('executionId' in data) metadata.executionId = data.executionId; } this.logger.debug(`Published pubsub msg: ${msgName}`, metadata); diff --git a/packages/cli/src/scaling/pubsub/pubsub-handler.ts b/packages/cli/src/scaling/pubsub/pubsub-handler.ts index deeed5b584..70b5f67f72 100644 --- a/packages/cli/src/scaling/pubsub/pubsub-handler.ts +++ b/packages/cli/src/scaling/pubsub/pubsub-handler.ts @@ -59,9 +59,12 @@ export class PubSubHandler { ...this.commonHandlers, ...this.multiMainHandlers, 'response-to-get-worker-status': async (payload) => - this.push.broadcast('sendWorkerStatusMessage', { - workerId: payload.senderId, - status: payload, + this.push.broadcast({ + type: 'sendWorkerStatusMessage', + data: { + workerId: payload.senderId, + status: payload, + }, }), }); @@ -113,7 +116,7 @@ export class PubSubHandler { shouldPublish: false, // prevent leader from re-publishing message }); - this.push.broadcast('workflowActivated', { workflowId }); + this.push.broadcast({ type: 'workflowActivated', data: { workflowId } }); await this.publisher.publishCommand({ command: 'display-workflow-activation', @@ -125,7 +128,10 @@ export class PubSubHandler { await this.workflowRepository.update(workflowId, { active: false }); - this.push.broadcast('workflowFailedToActivate', { workflowId, errorMessage: message }); + this.push.broadcast({ + type: 'workflowFailedToActivate', + data: { workflowId, errorMessage: message }, + }); await this.publisher.publishCommand({ command: 'display-workflow-activation-error', @@ -139,7 +145,7 @@ export class PubSubHandler { await this.activeWorkflowManager.removeActivationError(workflowId); await this.activeWorkflowManager.removeWorkflowTriggersAndPollers(workflowId); - this.push.broadcast('workflowDeactivated', { workflowId }); + this.push.broadcast({ type: 'workflowDeactivated', data: { workflowId } }); // instruct followers to show workflow deactivation in UI await this.publisher.publishCommand({ @@ -148,15 +154,15 @@ export class PubSubHandler { }); }, 'display-workflow-activation': async ({ workflowId }) => - this.push.broadcast('workflowActivated', { workflowId }), + this.push.broadcast({ type: 'workflowActivated', data: { workflowId } }), 'display-workflow-deactivation': async ({ workflowId }) => - this.push.broadcast('workflowDeactivated', { workflowId }), + this.push.broadcast({ type: 'workflowDeactivated', data: { workflowId } }), 'display-workflow-activation-error': async ({ workflowId, errorMessage }) => - this.push.broadcast('workflowFailedToActivate', { workflowId, errorMessage }), - 'relay-execution-lifecycle-event': async ({ type, args, pushRef }) => { + this.push.broadcast({ type: 'workflowFailedToActivate', data: { workflowId, errorMessage } }), + 'relay-execution-lifecycle-event': async ({ pushRef, ...pushMsg }) => { if (!this.push.getBackend().hasPushRef(pushRef)) return; - this.push.send(type, args, pushRef); + this.push.send(pushMsg, pushRef); }, 'clear-test-webhooks': async ({ webhookKey, workflowEntity, pushRef }) => { if (!this.push.getBackend().hasPushRef(pushRef)) return; diff --git a/packages/cli/src/scaling/pubsub/subscriber.service.ts b/packages/cli/src/scaling/pubsub/subscriber.service.ts index 248c1198d2..0ce343c139 100644 --- a/packages/cli/src/scaling/pubsub/subscriber.service.ts +++ b/packages/cli/src/scaling/pubsub/subscriber.service.ts @@ -95,10 +95,10 @@ export class Subscriber { const metadata: LogMetadata = { msg: msgName, channel }; if ('command' in msg && msg.command === 'relay-execution-lifecycle-event') { - const { args, type } = msg.payload; + const { data, type } = msg.payload; msgName += ` (${type})`; metadata.type = type; - metadata.executionId = args.executionId; + if ('executionId' in data) metadata.executionId = data.executionId; } this.logger.debug(`Received pubsub msg: ${msgName}`, metadata); diff --git a/packages/cli/src/webhooks/test-webhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts index ad642a17c3..b90b1db59d 100644 --- a/packages/cli/src/webhooks/test-webhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -142,8 +142,7 @@ export class TestWebhooks implements IWebhookManager { // Inform editor-ui that webhook got received if (pushRef !== undefined) { this.push.send( - 'testWebhookReceived', - { workflowId: webhook?.workflowId, executionId }, + { type: 'testWebhookReceived', data: { workflowId: webhook?.workflowId, executionId } }, pushRef, ); } @@ -354,7 +353,7 @@ export class TestWebhooks implements IWebhookManager { if (pushRef !== undefined) { try { - this.push.send('testWebhookDeleted', { workflowId }, pushRef); + this.push.send({ type: 'testWebhookDeleted', data: { workflowId } }, pushRef); } catch { // Could not inform editor, probably is not connected anymore. So simply go on. } diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index d020b4d868..29c8d67502 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import type { PushType } from '@n8n/api-types'; +import type { PushMessage, PushType } from '@n8n/api-types'; import { GlobalConfig } from '@n8n/config'; import { stringify } from 'flatted'; import { ErrorReporter, WorkflowExecute, isObjectLiteral } from 'n8n-core'; @@ -262,7 +262,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { workflowId: this.workflowData.id, }); - pushInstance.send('nodeExecuteBefore', { executionId, nodeName }, pushRef); + pushInstance.send({ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, pushRef); }, ], nodeExecuteAfter: [ @@ -279,7 +279,10 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { workflowId: this.workflowData.id, }); - pushInstance.send('nodeExecuteAfter', { executionId, nodeName, data }, pushRef); + pushInstance.send( + { type: 'nodeExecuteAfter', data: { executionId, nodeName, data } }, + pushRef, + ); }, ], workflowExecuteBefore: [ @@ -296,17 +299,19 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { return; } pushInstance.send( - 'executionStarted', { - executionId, - mode: this.mode, - startedAt: new Date(), - retryOf: this.retryOf, - workflowId, - workflowName, - flattedRunData: data?.resultData.runData - ? stringify(data.resultData.runData) - : stringify({}), + type: 'executionStarted', + data: { + executionId, + mode: this.mode, + startedAt: new Date(), + retryOf: this.retryOf, + workflowId, + workflowName, + flattedRunData: data?.resultData.runData + ? stringify(data.resultData.runData) + : stringify({}), + }, }, pushRef, ); @@ -326,12 +331,11 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { const { status } = fullRunData; if (status === 'waiting') { - pushInstance.send('executionWaiting', { executionId }, pushRef); + pushInstance.send({ type: 'executionWaiting', data: { executionId } }, pushRef); } else { const rawData = stringify(fullRunData.data); pushInstance.send( - 'executionFinished', - { executionId, workflowId, status, rawData }, + { type: 'executionFinished', data: { executionId, workflowId, status, rawData } }, pushRef, ); } @@ -974,7 +978,7 @@ export function sendDataToUI(type: PushType, data: IDataObject | IDataObject[]) // Push data to session which started workflow try { const pushInstance = Container.get(Push); - pushInstance.send(type, data, pushRef); + pushInstance.send({ type, data } as PushMessage, pushRef); } catch (error) { const logger = Container.get(Logger); logger.warn(`There was a problem sending message to UI: ${error.message}`); diff --git a/packages/cli/test/integration/collaboration/collaboration.service.test.ts b/packages/cli/test/integration/collaboration/collaboration.service.test.ts index df5f901f28..ab7a8314b3 100644 --- a/packages/cli/test/integration/collaboration/collaboration.service.test.ts +++ b/packages/cli/test/integration/collaboration/collaboration.service.test.ts @@ -78,37 +78,41 @@ describe('CollaborationService', () => { // Assert expect(sendToUsersSpy).toHaveBeenNthCalledWith( 1, - 'collaboratorsChanged', { - collaborators: [ - { - lastSeen: expect.any(String), - user: owner.toIUser(), - }, - ], - workflowId: workflow.id, + type: 'collaboratorsChanged', + data: { + collaborators: [ + { + lastSeen: expect.any(String), + user: owner.toIUser(), + }, + ], + workflowId: workflow.id, + }, }, [owner.id], ); expect(sendToUsersSpy).toHaveBeenNthCalledWith( 2, - 'collaboratorsChanged', { - collaborators: expect.arrayContaining([ - expect.objectContaining({ - lastSeen: expect.any(String), - user: expect.objectContaining({ - id: owner.id, + type: 'collaboratorsChanged', + data: { + collaborators: expect.arrayContaining([ + expect.objectContaining({ + lastSeen: expect.any(String), + user: expect.objectContaining({ + id: owner.id, + }), }), - }), - expect.objectContaining({ - lastSeen: expect.any(String), - user: expect.objectContaining({ - id: memberWithAccess.id, + expect.objectContaining({ + lastSeen: expect.any(String), + user: expect.objectContaining({ + id: memberWithAccess.id, + }), }), - }), - ]), - workflowId: workflow.id, + ]), + workflowId: workflow.id, + }, }, [owner.id, memberWithAccess.id], ); @@ -151,17 +155,19 @@ describe('CollaborationService', () => { // Assert expect(sendToUsersSpy).toHaveBeenCalledWith( - 'collaboratorsChanged', { - collaborators: expect.arrayContaining([ - expect.objectContaining({ - lastSeen: expect.any(String), - user: expect.objectContaining({ - id: memberWithAccess.id, + type: 'collaboratorsChanged', + data: { + collaborators: expect.arrayContaining([ + expect.objectContaining({ + lastSeen: expect.any(String), + user: expect.objectContaining({ + id: memberWithAccess.id, + }), }), - }), - ]), - workflowId: workflow.id, + ]), + workflowId: workflow.id, + }, }, [memberWithAccess.id], ); From b4c77f27b66275ddb58138e8d2fe1509265e9652 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Mon, 23 Dec 2024 13:03:24 +0100 Subject: [PATCH 08/66] fix: Set correct default for added Resource Mapper boolean fields (#12344) --- .../src/components/ResourceMapper/ResourceMapper.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index 7487f1d8d4..835e4a87a3 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -493,14 +493,20 @@ function addField(name: string): void { if (name === 'removeAllFields') { return removeAllFields(); } + const schema = state.paramValue.schema; + const field = schema.find((f) => f.id === name); + state.paramValue.value = { ...state.paramValue.value, - [name]: null, + // We only supply boolean defaults since it's a switch that cannot be null in `Fixed` mode + // Other defaults may break backwards compatibility as we'd remove the implicit passthrough + // mode you get when the field exists, but is empty in `Fixed` mode. + [name]: field?.type === 'boolean' ? false : null, }; - const field = state.paramValue.schema.find((f) => f.id === name); + if (field) { field.removed = false; - state.paramValue.schema.splice(state.paramValue.schema.indexOf(field), 1, field); + schema.splice(schema.indexOf(field), 1, field); } emitValueChanged(); } From 471d7b9420c17cd8aa032232d07f6f8b039ead13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 23 Dec 2024 13:46:13 +0100 Subject: [PATCH 09/66] refactor(core): Move Logger to `core` (no-changelog) (#12310) --- packages/@n8n/task-runner/src/start.ts | 2 +- packages/cli/package.json | 4 +- packages/cli/src/abstract-server.ts | 2 +- packages/cli/src/active-executions.ts | 2 +- packages/cli/src/active-workflow-manager.ts | 2 +- packages/cli/src/auth/auth.service.ts | 2 +- packages/cli/src/commands/base-command.ts | 2 +- .../src/commands/db/__tests__/revert.test.ts | 2 +- packages/cli/src/commands/db/revert.ts | 2 +- packages/cli/src/commands/worker.ts | 3 +- .../concurrency-control.service.ts | 2 +- packages/cli/src/config/index.ts | 25 ++++++---- .../cli/src/controllers/auth.controller.ts | 2 +- .../cli/src/controllers/e2e.controller.ts | 2 +- .../src/controllers/invitation.controller.ts | 2 +- packages/cli/src/controllers/me.controller.ts | 2 +- .../oauth1-credential.controller.test.ts | 2 +- .../oauth2-credential.controller.test.ts | 2 +- .../oauth/abstract-oauth.controller.ts | 3 +- .../cli/src/controllers/owner.controller.ts | 2 +- .../controllers/password-reset.controller.ts | 2 +- .../cli/src/controllers/users.controller.ts | 2 +- .../workflow-statistics.controller.ts | 2 +- packages/cli/src/crash-journal.ts | 3 +- packages/cli/src/credentials-overwrites.ts | 2 +- .../src/credentials/credentials.controller.ts | 2 +- .../src/credentials/credentials.service.ts | 3 +- .../repositories/execution.repository.ts | 3 +- .../databases/subscribers/user-subscriber.ts | 4 +- packages/cli/src/databases/types.ts | 3 +- .../src/databases/utils/migration-helpers.ts | 3 +- .../src/deprecation/deprecation.service.ts | 3 +- .../__tests__/source-control.service.test.ts | 3 +- .../source-control-export.service.ee.ts | 3 +- .../source-control-git.service.ee.ts | 2 +- .../source-control-helper.ee.ts | 2 +- .../source-control-import.service.ee.ts | 3 +- .../source-control-preferences.service.ee.ts | 3 +- .../source-control.service.ee.ts | 2 +- .../message-event-bus-destination-from-db.ts | 2 +- ...message-event-bus-destination-syslog.ee.ts | 3 +- .../message-event-bus-destination.ee.ts | 2 +- .../message-event-bus-log-writer.ts | 3 +- .../message-event-bus/message-event-bus.ts | 2 +- .../__tests__/save-execution-progress.test.ts | 2 +- .../restore-binary-data-id.ts | 3 +- .../save-execution-progress.ts | 3 +- .../shared/shared-hook-functions.ts | 2 +- .../execution-recovery.service.test.ts | 2 +- .../executions/execution-recovery.service.ts | 3 +- .../cli/src/executions/execution.service.ts | 2 +- .../external-secrets-manager.ee.ts | 3 +- .../aws-secrets/aws-secrets-manager.ts | 2 +- .../azure-key-vault/azure-key-vault.ts | 2 +- .../gcp-secrets-manager.ts | 2 +- .../src/external-secrets/providers/vault.ts | 2 +- packages/cli/src/help.ts | 4 +- packages/cli/src/ldap/ldap.service.ee.ts | 3 +- packages/cli/src/license.ts | 3 +- packages/cli/src/license/license.service.ts | 2 +- .../cli/src/load-nodes-and-credentials.ts | 2 +- packages/cli/src/logging/constants.ts | 3 -- packages/cli/src/logging/types.ts | 14 ------ packages/cli/src/manual-execution.service.ts | 3 +- .../src/push/__tests__/websocket.push.test.ts | 2 +- packages/cli/src/push/abstract.push.ts | 3 +- packages/cli/src/response-helper.ts | 3 +- ...nner-process-restart-loop-detector.test.ts | 2 +- .../__tests__/task-runner-process.test.ts | 2 +- packages/cli/src/runners/runner-ws-server.ts | 2 +- .../cli/src/runners/task-broker.service.ts | 2 +- .../cli/src/runners/task-runner-module.ts | 3 +- .../cli/src/runners/task-runner-process.ts | 2 +- .../cli/src/runners/task-runner-server.ts | 2 +- packages/cli/src/scaling/job-processor.ts | 3 +- .../cli/src/scaling/multi-main-setup.ee.ts | 3 +- .../src/scaling/pubsub/publisher.service.ts | 5 +- .../src/scaling/pubsub/subscriber.service.ts | 5 +- packages/cli/src/scaling/scaling.service.ts | 3 +- packages/cli/src/scaling/worker-server.ts | 3 +- .../risk-reporters/instance-risk-reporter.ts | 3 +- .../src/services/active-workflows.service.ts | 2 +- .../services/community-packages.service.ts | 3 +- .../services/credentials-tester.service.ts | 9 +++- packages/cli/src/services/frontend.service.ts | 3 +- packages/cli/src/services/import.service.ts | 2 +- .../src/services/pruning/pruning.service.ts | 3 +- .../cli/src/services/redis-client.service.ts | 2 +- packages/cli/src/services/user.service.ts | 2 +- .../services/workflow-statistics.service.ts | 2 +- packages/cli/src/shutdown/shutdown.service.ts | 2 +- .../sso/saml/__tests__/saml-validator.test.ts | 3 +- .../saml/__tests__/saml.service.ee.test.ts | 2 +- packages/cli/src/sso/saml/saml-validator.ts | 3 +- packages/cli/src/sso/saml/saml.service.ee.ts | 2 +- .../subworkflow-policy-checker.service.ts | 2 +- packages/cli/src/telemetry/index.ts | 3 +- .../src/user-management/email/node-mailer.ts | 4 +- .../email/user-management-mailer.ts | 2 +- packages/cli/src/wait-tracker.ts | 3 +- packages/cli/src/webhooks/live-webhooks.ts | 2 +- packages/cli/src/webhooks/waiting-webhooks.ts | 2 +- packages/cli/src/webhooks/webhook-helpers.ts | 3 +- packages/cli/src/webhooks/webhook.service.ts | 3 +- .../src/workflow-execute-additional-data.ts | 3 +- packages/cli/src/workflow-runner.ts | 3 +- .../workflows/workflow-execution.service.ts | 3 +- .../workflow-history.service.ee.ts | 2 +- .../workflows/workflow-static-data.service.ts | 3 +- .../cli/src/workflows/workflow.service.ee.ts | 2 +- .../cli/src/workflows/workflow.service.ts | 3 +- .../cli/src/workflows/workflows.controller.ts | 2 +- .../active-workflow-manager.test.ts | 2 +- .../test/integration/pruning.service.test.ts | 2 +- .../test/integration/shared/db/workflows.ts | 1 + .../integration/shared/utils/test-server.ts | 2 +- packages/cli/test/shared/mocking.ts | 3 +- packages/core/package.json | 3 ++ packages/core/src/ActiveWorkflows.ts | 9 ++-- packages/core/src/Constants.ts | 4 ++ packages/core/src/DirectoryLoader.ts | 20 ++++---- packages/core/src/InstanceSettings.ts | 36 +++++++------- packages/core/src/InstanceSettingsConfig.ts | 16 +++++++ packages/core/src/NodeExecuteFunctions.ts | 20 ++++---- .../src/ObjectStore/ObjectStore.service.ee.ts | 6 ++- packages/core/src/SerializedBuffer.ts | 2 +- .../src/__tests__/ActiveWorkflows.test.ts | 7 ++- packages/core/src/error-reporter.ts | 7 +-- packages/core/src/index.ts | 1 + .../src/logging/__tests__/logger.test.ts} | 23 ++++----- .../src/logging/logger.ts} | 30 ++++++++---- .../node-execution-context.ts | 5 +- packages/core/test/InstanceSettings.test.ts | 17 +++++-- .../core/test/ObjectStore.service.test.ts | 3 +- packages/core/test/error-reporter.test.ts | 3 +- packages/core/tsconfig.json | 1 + packages/nodes-base/nodes/Ldap/Helpers.ts | 6 +-- packages/nodes-base/nodes/Ldap/Ldap.node.ts | 9 ++-- packages/workflow/package.json | 3 +- packages/workflow/src/Interfaces.ts | 15 +++++- pnpm-lock.yaml | 48 ++++++++++++------- pnpm-workspace.yaml | 2 + 142 files changed, 328 insertions(+), 302 deletions(-) delete mode 100644 packages/cli/src/logging/constants.ts delete mode 100644 packages/cli/src/logging/types.ts rename packages/{cli/src/logging/__tests__/logger.service.test.ts => core/src/logging/__tests__/logger.test.ts} (93%) rename packages/{cli/src/logging/logger.service.ts => core/src/logging/logger.ts} (89%) diff --git a/packages/@n8n/task-runner/src/start.ts b/packages/@n8n/task-runner/src/start.ts index 391b6ba156..93ff742250 100644 --- a/packages/@n8n/task-runner/src/start.ts +++ b/packages/@n8n/task-runner/src/start.ts @@ -56,7 +56,7 @@ void (async function start() { if (config.sentryConfig.sentryDsn) { const { ErrorReporter } = await import('n8n-core'); - errorReporter = new ErrorReporter(); + errorReporter = Container.get(ErrorReporter); await errorReporter.init('task_runner', config.sentryConfig.sentryDsn); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 8e9ff0f7ca..e005ce3025 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -104,7 +104,6 @@ "bcryptjs": "2.4.3", "bull": "4.12.1", "cache-manager": "5.2.3", - "callsites": "3.1.0", "change-case": "4.1.2", "class-transformer": "0.5.1", "class-validator": "0.14.0", @@ -149,7 +148,7 @@ "p-cancelable": "2.1.1", "p-lazy": "3.1.0", "pg": "8.12.0", - "picocolors": "1.0.1", + "picocolors": "catalog:", "pkce-challenge": "3.0.0", "posthog-node": "3.2.1", "prom-client": "13.2.0", @@ -169,7 +168,6 @@ "typedi": "catalog:", "uuid": "catalog:", "validator": "13.7.0", - "winston": "3.14.2", "ws": "8.17.1", "xml2js": "catalog:", "xmllint-wasm": "3.0.1", diff --git a/packages/cli/src/abstract-server.ts b/packages/cli/src/abstract-server.ts index f4a8a5b2cc..aadd41fb05 100644 --- a/packages/cli/src/abstract-server.ts +++ b/packages/cli/src/abstract-server.ts @@ -5,6 +5,7 @@ import { engine as expressHandlebars } from 'express-handlebars'; import { readFile } from 'fs/promises'; import type { Server } from 'http'; import isbot from 'isbot'; +import { Logger } from 'n8n-core'; import { Container, Service } from 'typedi'; import config from '@/config'; @@ -12,7 +13,6 @@ import { N8N_VERSION, TEMPLATES_DIR, inDevelopment, inTest } from '@/constants'; import * as Db from '@/db'; import { OnShutdown } from '@/decorators/on-shutdown'; import { ExternalHooks } from '@/external-hooks'; -import { Logger } from '@/logging/logger.service'; import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; import { send, sendErrorResponse } from '@/response-helper'; import { LiveWebhooks } from '@/webhooks/live-webhooks'; diff --git a/packages/cli/src/active-executions.ts b/packages/cli/src/active-executions.ts index bc18eade16..a3fdcf6fee 100644 --- a/packages/cli/src/active-executions.ts +++ b/packages/cli/src/active-executions.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import type { IDeferredPromise, IExecuteResponsePromiseData, @@ -18,7 +19,6 @@ import type { IExecutionDb, IExecutionsCurrentSummary, } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { isWorkflowIdValid } from '@/utils'; import { ConcurrencyControlService } from './concurrency/concurrency-control.service'; diff --git a/packages/cli/src/active-workflow-manager.ts b/packages/cli/src/active-workflow-manager.ts index 6ef3753af7..368e2987c8 100644 --- a/packages/cli/src/active-workflow-manager.ts +++ b/packages/cli/src/active-workflow-manager.ts @@ -3,6 +3,7 @@ import { ActiveWorkflows, ErrorReporter, InstanceSettings, + Logger, PollContext, TriggerContext, } from 'n8n-core'; @@ -42,7 +43,6 @@ import { OnShutdown } from '@/decorators/on-shutdown'; import { ExecutionService } from '@/executions/execution.service'; import { ExternalHooks } from '@/external-hooks'; import type { IWorkflowDb } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import { ActiveWorkflowsService } from '@/services/active-workflows.service'; diff --git a/packages/cli/src/auth/auth.service.ts b/packages/cli/src/auth/auth.service.ts index 492e22ab53..3a2e4fb0cb 100644 --- a/packages/cli/src/auth/auth.service.ts +++ b/packages/cli/src/auth/auth.service.ts @@ -2,6 +2,7 @@ import { GlobalConfig } from '@n8n/config'; import { createHash } from 'crypto'; import type { NextFunction, Response } from 'express'; import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; +import { Logger } from 'n8n-core'; import Container, { Service } from 'typedi'; import config from '@/config'; @@ -12,7 +13,6 @@ import { UserRepository } from '@/databases/repositories/user.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import type { AuthenticatedRequest } from '@/requests'; import { JwtService } from '@/services/jwt.service'; import { UrlService } from '@/services/url.service'; diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index 286fec1de6..a10c386f42 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -4,6 +4,7 @@ import { Command, Errors } from '@oclif/core'; import { BinaryDataService, InstanceSettings, + Logger, ObjectStoreService, DataDeduplicationService, ErrorReporter, @@ -25,7 +26,6 @@ import { ExternalHooks } from '@/external-hooks'; import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import { PostHogClient } from '@/posthog'; import { ShutdownService } from '@/shutdown/shutdown.service'; diff --git a/packages/cli/src/commands/db/__tests__/revert.test.ts b/packages/cli/src/commands/db/__tests__/revert.test.ts index ce3911b2b6..8fdabafbec 100644 --- a/packages/cli/src/commands/db/__tests__/revert.test.ts +++ b/packages/cli/src/commands/db/__tests__/revert.test.ts @@ -1,10 +1,10 @@ import type { Migration, MigrationExecutor } from '@n8n/typeorm'; import { type DataSource } from '@n8n/typeorm'; import { mock } from 'jest-mock-extended'; +import { Logger } from 'n8n-core'; import { main } from '@/commands/db/revert'; import type { IrreversibleMigration, ReversibleMigration } from '@/databases/types'; -import { Logger } from '@/logging/logger.service'; import { mockInstance } from '@test/mocking'; const logger = mockInstance(Logger); diff --git a/packages/cli/src/commands/db/revert.ts b/packages/cli/src/commands/db/revert.ts index 4510044405..bc9e0f6b3f 100644 --- a/packages/cli/src/commands/db/revert.ts +++ b/packages/cli/src/commands/db/revert.ts @@ -3,12 +3,12 @@ import type { DataSourceOptions as ConnectionOptions } from '@n8n/typeorm'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { MigrationExecutor, DataSource as Connection } from '@n8n/typeorm'; import { Command, Flags } from '@oclif/core'; +import { Logger } from 'n8n-core'; import { Container } from 'typedi'; import { getConnectionOptions } from '@/databases/config'; import type { Migration } from '@/databases/types'; import { wrapMigration } from '@/databases/utils/migration-helpers'; -import { Logger } from '@/logging/logger.service'; // This function is extracted to make it easier to unit test it. // Mocking turned into a mess due to this command using typeorm and the db diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 64c5a34dae..ac413077d2 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -7,7 +7,6 @@ import { WorkerMissingEncryptionKey } from '@/errors/worker-missing-encryption-k import { EventMessageGeneric } from '@/eventbus/event-message-classes/event-message-generic'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; -import { Logger } from '@/logging/logger.service'; import { PubSubHandler } from '@/scaling/pubsub/pubsub-handler'; import { Subscriber } from '@/scaling/pubsub/subscriber.service'; import type { ScalingService } from '@/scaling/scaling.service'; @@ -67,7 +66,7 @@ export class Worker extends BaseCommand { super(argv, cmdConfig); - this.logger = Container.get(Logger).scoped('scaling'); + this.logger = this.logger.scoped('scaling'); } async init() { diff --git a/packages/cli/src/concurrency/concurrency-control.service.ts b/packages/cli/src/concurrency/concurrency-control.service.ts index 3896daad2e..ede6cf8997 100644 --- a/packages/cli/src/concurrency/concurrency-control.service.ts +++ b/packages/cli/src/concurrency/concurrency-control.service.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import type { WorkflowExecuteMode as ExecutionMode } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -7,7 +8,6 @@ import { InvalidConcurrencyLimitError } from '@/errors/invalid-concurrency-limit import { UnknownExecutionModeError } from '@/errors/unknown-execution-mode.error'; import { EventService } from '@/events/event.service'; import type { IExecutingWorkflowData } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { Telemetry } from '@/telemetry'; import { ConcurrencyQueue } from './concurrency-queue'; diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 63497600fe..1ba49a1ef4 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -3,7 +3,9 @@ import convict from 'convict'; import { flatten } from 'flat'; import { readFileSync } from 'fs'; import merge from 'lodash/merge'; +import { Logger } from 'n8n-core'; import { ApplicationError, setGlobalState } from 'n8n-workflow'; +import assert from 'node:assert'; import colors from 'picocolors'; import { Container } from 'typedi'; @@ -31,13 +33,15 @@ const config = convict(schema, { args: [] }); // eslint-disable-next-line @typescript-eslint/unbound-method config.getEnv = config.get; +const logger = Container.get(Logger); +const globalConfig = Container.get(GlobalConfig); + // Load overwrites when not in tests if (!inE2ETests && !inTest) { // Overwrite default configuration with settings which got defined in // optional configuration files const { N8N_CONFIG_FILES } = process.env; if (N8N_CONFIG_FILES !== undefined) { - const globalConfig = Container.get(GlobalConfig); const configFiles = N8N_CONFIG_FILES.split(','); for (const configFile of configFiles) { if (!configFile) continue; @@ -58,9 +62,10 @@ if (!inE2ETests && !inTest) { } } } - console.debug('Loaded config overwrites from', configFile); + logger.debug(`Loaded config overwrites from ${configFile}`); } catch (error) { - console.error('Error loading config file', configFile, error); + assert(error instanceof Error); + logger.error(`Error loading config file ${configFile}`, { error }); } } } @@ -96,7 +101,7 @@ config.validate({ const userManagement = config.get('userManagement'); if (userManagement.jwtRefreshTimeoutHours >= userManagement.jwtSessionDurationHours) { if (!inTest) - console.warn( + logger.warn( 'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0 for now.', ); @@ -105,16 +110,16 @@ if (userManagement.jwtRefreshTimeoutHours >= userManagement.jwtSessionDurationHo const executionProcess = config.getEnv('executions.process'); if (executionProcess) { - console.error( - colors.yellow('Please unset the deprecated env variable'), - colors.bold(colors.yellow('EXECUTIONS_PROCESS')), + logger.error( + colors.yellow('Please unset the deprecated env variable') + + colors.bold(colors.yellow('EXECUTIONS_PROCESS')), ); } if (executionProcess === 'own') { - console.error( + logger.error( colors.bold(colors.red('Application failed to start because "Own" mode has been removed.')), ); - console.error( + logger.error( colors.red( 'If you need the isolation and performance gains, please consider using queue mode instead.\n\n', ), @@ -123,7 +128,7 @@ if (executionProcess === 'own') { } setGlobalState({ - defaultTimezone: Container.get(GlobalConfig).generic.timezone, + defaultTimezone: globalConfig.generic.timezone, }); // eslint-disable-next-line import/no-default-export diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 46ee73a562..faf24ed669 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -1,4 +1,5 @@ import { Response } from 'express'; +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import validator from 'validator'; @@ -14,7 +15,6 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { EventService } from '@/events/event.service'; import type { PublicUser } from '@/interfaces'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { MfaService } from '@/mfa/mfa.service'; import { PostHogClient } from '@/posthog'; import { AuthenticatedRequest, LoginRequest, UserRequest } from '@/requests'; diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index a61342320d..9d0404d312 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -1,5 +1,6 @@ import type { PushMessage } from '@n8n/api-types'; import { Request } from 'express'; +import { Logger } from 'n8n-core'; import Container from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -14,7 +15,6 @@ import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus' import type { BooleanLicenseFeature, NumericLicenseFeature } from '@/interfaces'; import type { FeatureReturnType } from '@/license'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { MfaService } from '@/mfa/mfa.service'; import { Push } from '@/push'; import type { UserSetupPayload } from '@/requests'; diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index cbd2afb9f4..24cff10c64 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -1,4 +1,5 @@ import { Response } from 'express'; +import { Logger } from 'n8n-core'; import validator from 'validator'; import { AuthService } from '@/auth/auth.service'; @@ -12,7 +13,6 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { PostHogClient } from '@/posthog'; import { UserRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index a7fb7235fd..f56d62aebd 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -5,6 +5,7 @@ import { } from '@n8n/api-types'; import { plainToInstance } from 'class-transformer'; import { Response } from 'express'; +import { Logger } from 'n8n-core'; import { AuthService } from '@/auth/auth.service'; import type { User } from '@/databases/entities/user'; @@ -16,7 +17,6 @@ import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import type { PublicUser } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { MfaService } from '@/mfa/mfa.service'; import { AuthenticatedRequest, MeRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts index 2d76642266..c7990ddbfd 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts @@ -2,6 +2,7 @@ import Csrf from 'csrf'; import type { Response } from 'express'; import { mock } from 'jest-mock-extended'; import { Cipher } from 'n8n-core'; +import { Logger } from 'n8n-core'; import nock from 'nock'; import Container from 'typedi'; @@ -16,7 +17,6 @@ import { VariablesService } from '@/environments/variables/variables.service.ee' import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; -import { Logger } from '@/logging/logger.service'; import type { OAuthRequest } from '@/requests'; import { SecretsHelper } from '@/secrets-helpers'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts index b2bd987fb0..ca9e4db5c6 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts @@ -2,6 +2,7 @@ import Csrf from 'csrf'; import { type Response } from 'express'; import { mock } from 'jest-mock-extended'; import { Cipher } from 'n8n-core'; +import { Logger } from 'n8n-core'; import nock from 'nock'; import Container from 'typedi'; @@ -16,7 +17,6 @@ import { VariablesService } from '@/environments/variables/variables.service.ee' import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; -import { Logger } from '@/logging/logger.service'; import type { OAuthRequest } from '@/requests'; import { SecretsHelper } from '@/secrets-helpers'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts b/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts index 3f5c20dfc3..97fb7be24a 100644 --- a/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts +++ b/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts @@ -1,7 +1,7 @@ import { GlobalConfig } from '@n8n/config'; import Csrf from 'csrf'; import type { Response } from 'express'; -import { Credentials } from 'n8n-core'; +import { Credentials, Logger } from 'n8n-core'; import type { ICredentialDataDecryptedObject, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import { jsonParse, ApplicationError } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -16,7 +16,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import type { ICredentialsDb } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import type { AuthenticatedRequest, OAuthRequest } from '@/requests'; import { UrlService } from '@/services/url.service'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 47d50ad3f0..1db250c488 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -1,4 +1,5 @@ import { Response } from 'express'; +import { Logger } from 'n8n-core'; import validator from 'validator'; import { AuthService } from '@/auth/auth.service'; @@ -9,7 +10,6 @@ import { GlobalScope, Post, RestController } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; import { validateEntity } from '@/generic-helpers'; -import { Logger } from '@/logging/logger.service'; import { PostHogClient } from '@/posthog'; import { OwnerRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/src/controllers/password-reset.controller.ts b/packages/cli/src/controllers/password-reset.controller.ts index 2179ff3d9e..cb5e2c6f8b 100644 --- a/packages/cli/src/controllers/password-reset.controller.ts +++ b/packages/cli/src/controllers/password-reset.controller.ts @@ -1,4 +1,5 @@ import { Response } from 'express'; +import { Logger } from 'n8n-core'; import validator from 'validator'; import { AuthService } from '@/auth/auth.service'; @@ -13,7 +14,6 @@ import { UnprocessableRequestError } from '@/errors/response-errors/unprocessabl import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { MfaService } from '@/mfa/mfa.service'; import { PasswordResetRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 8e19be894d..4cfa18a1e3 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -1,5 +1,6 @@ import { RoleChangeRequestDto, SettingsUpdateRequestDto } from '@n8n/api-types'; import { Response } from 'express'; +import { Logger } from 'n8n-core'; import { AuthService } from '@/auth/auth.service'; import { CredentialsService } from '@/credentials/credentials.service'; @@ -18,7 +19,6 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import type { PublicUser } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { listQueryMiddleware } from '@/middlewares'; import { AuthenticatedRequest, ListQuery, UserRequest } from '@/requests'; import { ProjectService } from '@/services/project.service'; diff --git a/packages/cli/src/controllers/workflow-statistics.controller.ts b/packages/cli/src/controllers/workflow-statistics.controller.ts index 58c99727db..b14afc9179 100644 --- a/packages/cli/src/controllers/workflow-statistics.controller.ts +++ b/packages/cli/src/controllers/workflow-statistics.controller.ts @@ -1,4 +1,5 @@ import { Response, NextFunction } from 'express'; +import { Logger } from 'n8n-core'; import type { WorkflowStatistics } from '@/databases/entities/workflow-statistics'; import { StatisticsNames } from '@/databases/entities/workflow-statistics'; @@ -7,7 +8,6 @@ import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow- import { Get, Middleware, RestController } from '@/decorators'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IWorkflowStatisticsDataLoaded } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { StatisticsRequest } from './workflow-statistics.types'; diff --git a/packages/cli/src/crash-journal.ts b/packages/cli/src/crash-journal.ts index 577a2f34fe..8afae1e88c 100644 --- a/packages/cli/src/crash-journal.ts +++ b/packages/cli/src/crash-journal.ts @@ -1,12 +1,11 @@ import { existsSync } from 'fs'; import { mkdir, utimes, open, rm } from 'fs/promises'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { sleep } from 'n8n-workflow'; import { join, dirname } from 'path'; import { Container } from 'typedi'; import { inProduction } from '@/constants'; -import { Logger } from '@/logging/logger.service'; export const touchFile = async (filePath: string): Promise => { await mkdir(dirname(filePath), { recursive: true }); diff --git a/packages/cli/src/credentials-overwrites.ts b/packages/cli/src/credentials-overwrites.ts index ed1b492dc6..30f6bedfb8 100644 --- a/packages/cli/src/credentials-overwrites.ts +++ b/packages/cli/src/credentials-overwrites.ts @@ -1,11 +1,11 @@ import { GlobalConfig } from '@n8n/config'; +import { Logger } from 'n8n-core'; import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; import { deepCopy, jsonParse } from 'n8n-workflow'; import { Service } from 'typedi'; import { CredentialTypes } from '@/credential-types'; import type { ICredentialsOverwrite } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; @Service() export class CredentialsOverwrites { diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 76db501cf7..3868c3b87f 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -1,6 +1,7 @@ import { GlobalConfig } from '@n8n/config'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { In } from '@n8n/typeorm'; +import { Logger } from 'n8n-core'; import { deepCopy } from 'n8n-workflow'; import { z } from 'zod'; @@ -23,7 +24,6 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { EventService } from '@/events/event.service'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { listQueryMiddleware } from '@/middlewares'; import { CredentialRequest } from '@/requests'; import { NamingService } from '@/services/naming.service'; diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index f9bbf89e57..8261b13649 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -6,7 +6,7 @@ import { type FindOptionsRelations, type FindOptionsWhere, } from '@n8n/typeorm'; -import { Credentials } from 'n8n-core'; +import { Credentials, Logger } from 'n8n-core'; import type { ICredentialDataDecryptedObject, ICredentialsDecrypted, @@ -33,7 +33,6 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import type { ICredentialsDb } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { userHasScopes } from '@/permissions/check-access'; import type { CredentialRequest, ListQuery } from '@/requests'; import { CredentialsTester } from '@/services/credentials-tester.service'; diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index fbcb7de445..617dde9136 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -21,7 +21,7 @@ import { import { DateUtils } from '@n8n/typeorm/util/DateUtils'; import { parse, stringify } from 'flatted'; import pick from 'lodash/pick'; -import { BinaryDataService, ErrorReporter } from 'n8n-core'; +import { BinaryDataService, ErrorReporter, Logger } from 'n8n-core'; import { ExecutionCancelledError, ApplicationError } from 'n8n-workflow'; import type { AnnotationVote, @@ -42,7 +42,6 @@ import type { IExecutionFlattedDb, IExecutionResponse, } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { separate } from '@/utils'; import { ExecutionDataRepository } from './execution-data.repository'; diff --git a/packages/cli/src/databases/subscribers/user-subscriber.ts b/packages/cli/src/databases/subscribers/user-subscriber.ts index 1c55572b14..2dc6a1d8e2 100644 --- a/packages/cli/src/databases/subscribers/user-subscriber.ts +++ b/packages/cli/src/databases/subscribers/user-subscriber.ts @@ -1,11 +1,9 @@ import type { EntitySubscriberInterface, UpdateEvent } from '@n8n/typeorm'; import { EventSubscriber } from '@n8n/typeorm'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import { Container } from 'typedi'; -import { Logger } from '@/logging/logger.service'; - import { Project } from '../entities/project'; import { User } from '../entities/user'; import { UserRepository } from '../repositories/user.repository'; diff --git a/packages/cli/src/databases/types.ts b/packages/cli/src/databases/types.ts index 2bb1802bf2..dce7d9d243 100644 --- a/packages/cli/src/databases/types.ts +++ b/packages/cli/src/databases/types.ts @@ -1,8 +1,7 @@ import type { QueryRunner, ObjectLiteral } from '@n8n/typeorm'; +import type { Logger } from 'n8n-core'; import type { INodeTypes } from 'n8n-workflow'; -import type { Logger } from '@/logging/logger.service'; - import type { createSchemaBuilder } from './dsl'; export type DatabaseType = 'mariadb' | 'postgresdb' | 'mysqldb' | 'sqlite'; diff --git a/packages/cli/src/databases/utils/migration-helpers.ts b/packages/cli/src/databases/utils/migration-helpers.ts index 1093096f43..70839b9337 100644 --- a/packages/cli/src/databases/utils/migration-helpers.ts +++ b/packages/cli/src/databases/utils/migration-helpers.ts @@ -2,14 +2,13 @@ import { GlobalConfig } from '@n8n/config'; import type { ObjectLiteral } from '@n8n/typeorm'; import type { QueryRunner } from '@n8n/typeorm/query-runner/QueryRunner'; import { readFileSync, rmSync } from 'fs'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, jsonParse } from 'n8n-workflow'; import { Container } from 'typedi'; import { inTest } from '@/constants'; import { createSchemaBuilder } from '@/databases/dsl'; import type { BaseMigration, Migration, MigrationContext, MigrationFn } from '@/databases/types'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json'; diff --git a/packages/cli/src/deprecation/deprecation.service.ts b/packages/cli/src/deprecation/deprecation.service.ts index fef25ff0dd..b3c4cb7d21 100644 --- a/packages/cli/src/deprecation/deprecation.service.ts +++ b/packages/cli/src/deprecation/deprecation.service.ts @@ -1,8 +1,7 @@ +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import { Service } from 'typedi'; -import { Logger } from '@/logging/logger.service'; - type EnvVarName = string; type Deprecation = { diff --git a/packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts b/packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts index e1ebf0e56a..1599f7b4bd 100644 --- a/packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts +++ b/packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts @@ -1,12 +1,13 @@ import { mock } from 'jest-mock-extended'; import { InstanceSettings } from 'n8n-core'; +import { Container } from 'typedi'; import { SourceControlPreferencesService } from '@/environments/source-control/source-control-preferences.service.ee'; import { SourceControlService } from '@/environments/source-control/source-control.service.ee'; describe('SourceControlService', () => { const preferencesService = new SourceControlPreferencesService( - new InstanceSettings(mock()), + Container.get(InstanceSettings), mock(), mock(), ); diff --git a/packages/cli/src/environments/source-control/source-control-export.service.ee.ts b/packages/cli/src/environments/source-control/source-control-export.service.ee.ts index 03352410f4..cb678d534d 100644 --- a/packages/cli/src/environments/source-control/source-control-export.service.ee.ts +++ b/packages/cli/src/environments/source-control/source-control-export.service.ee.ts @@ -1,5 +1,5 @@ import { rmSync } from 'fs'; -import { Credentials, InstanceSettings } from 'n8n-core'; +import { Credentials, InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, type ICredentialDataDecryptedObject } from 'n8n-workflow'; import { writeFile as fsWriteFile, rm as fsRm } from 'node:fs/promises'; import path from 'path'; @@ -11,7 +11,6 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl import { TagRepository } from '@/databases/repositories/tag.repository'; import { WorkflowTagMappingRepository } from '@/databases/repositories/workflow-tag-mapping.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { Logger } from '@/logging/logger.service'; import { SOURCE_CONTROL_CREDENTIAL_EXPORT_FOLDER, diff --git a/packages/cli/src/environments/source-control/source-control-git.service.ee.ts b/packages/cli/src/environments/source-control/source-control-git.service.ee.ts index 99571cdd52..0d87d4c2d1 100644 --- a/packages/cli/src/environments/source-control/source-control-git.service.ee.ts +++ b/packages/cli/src/environments/source-control/source-control-git.service.ee.ts @@ -1,4 +1,5 @@ import { execSync } from 'child_process'; +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import path from 'path'; import type { @@ -14,7 +15,6 @@ import type { import { Service } from 'typedi'; import type { User } from '@/databases/entities/user'; -import { Logger } from '@/logging/logger.service'; import { OwnershipService } from '@/services/ownership.service'; import { diff --git a/packages/cli/src/environments/source-control/source-control-helper.ee.ts b/packages/cli/src/environments/source-control/source-control-helper.ee.ts index 00a9875741..6e8b92f09b 100644 --- a/packages/cli/src/environments/source-control/source-control-helper.ee.ts +++ b/packages/cli/src/environments/source-control/source-control-helper.ee.ts @@ -1,12 +1,12 @@ import { generateKeyPairSync } from 'crypto'; import { constants as fsConstants, mkdirSync, accessSync } from 'fs'; +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import { ok } from 'node:assert/strict'; import path from 'path'; import { Container } from 'typedi'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { isContainedWithin } from '@/utils/path-util'; import { diff --git a/packages/cli/src/environments/source-control/source-control-import.service.ee.ts b/packages/cli/src/environments/source-control/source-control-import.service.ee.ts index 2e7da80c13..10ae293b60 100644 --- a/packages/cli/src/environments/source-control/source-control-import.service.ee.ts +++ b/packages/cli/src/environments/source-control/source-control-import.service.ee.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { In } from '@n8n/typeorm'; import glob from 'fast-glob'; -import { Credentials, ErrorReporter, InstanceSettings } from 'n8n-core'; +import { Credentials, ErrorReporter, InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, jsonParse, ensureError } from 'n8n-workflow'; import { readFile as fsReadFile } from 'node:fs/promises'; import path from 'path'; @@ -23,7 +23,6 @@ import { VariablesRepository } from '@/databases/repositories/variables.reposito import { WorkflowTagMappingRepository } from '@/databases/repositories/workflow-tag-mapping.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { IWorkflowToImport } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { isUniqueConstraintError } from '@/response-helper'; import { assertNever } from '@/utils'; diff --git a/packages/cli/src/environments/source-control/source-control-preferences.service.ee.ts b/packages/cli/src/environments/source-control/source-control-preferences.service.ee.ts index 7c061b6c3c..ec46e02454 100644 --- a/packages/cli/src/environments/source-control/source-control-preferences.service.ee.ts +++ b/packages/cli/src/environments/source-control/source-control-preferences.service.ee.ts @@ -1,7 +1,7 @@ import type { ValidationError } from 'class-validator'; import { validate } from 'class-validator'; import { rm as fsRm } from 'fs/promises'; -import { Cipher, InstanceSettings } from 'n8n-core'; +import { Cipher, InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, jsonParse } from 'n8n-workflow'; import { writeFile, chmod, readFile } from 'node:fs/promises'; import path from 'path'; @@ -9,7 +9,6 @@ import Container, { Service } from 'typedi'; import config from '@/config'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; -import { Logger } from '@/logging/logger.service'; import { SOURCE_CONTROL_SSH_FOLDER, diff --git a/packages/cli/src/environments/source-control/source-control.service.ee.ts b/packages/cli/src/environments/source-control/source-control.service.ee.ts index e010210262..8fcb1f3571 100644 --- a/packages/cli/src/environments/source-control/source-control.service.ee.ts +++ b/packages/cli/src/environments/source-control/source-control.service.ee.ts @@ -1,4 +1,5 @@ import { writeFileSync } from 'fs'; +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import path from 'path'; import type { PushResult } from 'simple-git'; @@ -10,7 +11,6 @@ import type { Variables } from '@/databases/entities/variables'; import { TagRepository } from '@/databases/repositories/tag.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; -import { Logger } from '@/logging/logger.service'; import { SOURCE_CONTROL_DEFAULT_EMAIL, diff --git a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-from-db.ts b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-from-db.ts index 4046855f30..90049da1ff 100644 --- a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-from-db.ts +++ b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-from-db.ts @@ -1,8 +1,8 @@ +import { Logger } from 'n8n-core'; import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; import { Container } from 'typedi'; import type { EventDestinations } from '@/databases/entities/event-destinations'; -import { Logger } from '@/logging/logger.service'; import { MessageEventBusDestinationSentry } from './message-event-bus-destination-sentry.ee'; import { MessageEventBusDestinationSyslog } from './message-event-bus-destination-syslog.ee'; diff --git a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-syslog.ee.ts b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-syslog.ee.ts index 83db469d79..c0e7657e0f 100644 --- a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-syslog.ee.ts +++ b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-syslog.ee.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { Logger } from 'n8n-core'; import type { MessageEventBusDestinationOptions, MessageEventBusDestinationSyslogOptions, @@ -7,8 +8,6 @@ import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; import syslog from 'syslog-client'; import Container from 'typedi'; -import { Logger } from '@/logging/logger.service'; - import { MessageEventBusDestination } from './message-event-bus-destination.ee'; import { eventMessageGenericDestinationTestEvent } from '../event-message-classes/event-message-generic'; import type { MessageEventBus, MessageWithCallback } from '../message-event-bus/message-event-bus'; diff --git a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination.ee.ts b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination.ee.ts index 7b65767b04..c3aaf71173 100644 --- a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination.ee.ts +++ b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination.ee.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import type { INodeCredentials, MessageEventBusDestinationOptions } from 'n8n-workflow'; import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; import { Container } from 'typedi'; @@ -5,7 +6,6 @@ import { v4 as uuid } from 'uuid'; import { EventDestinationsRepository } from '@/databases/repositories/event-destinations.repository'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import type { EventMessageTypes } from '../event-message-classes'; import type { AbstractEventMessage } from '../event-message-classes/abstract-event-message'; diff --git a/packages/cli/src/eventbus/message-event-bus-writer/message-event-bus-log-writer.ts b/packages/cli/src/eventbus/message-event-bus-writer/message-event-bus-log-writer.ts index 3f3cb50b18..c418035a70 100644 --- a/packages/cli/src/eventbus/message-event-bus-writer/message-event-bus-log-writer.ts +++ b/packages/cli/src/eventbus/message-event-bus-writer/message-event-bus-log-writer.ts @@ -4,7 +4,7 @@ import { GlobalConfig } from '@n8n/config'; import { once as eventOnce } from 'events'; import { createReadStream, existsSync, rmSync } from 'fs'; import remove from 'lodash/remove'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { EventMessageTypeNames, jsonParse } from 'n8n-workflow'; import path, { parse } from 'path'; import readline from 'readline'; @@ -12,7 +12,6 @@ import Container from 'typedi'; import { Worker } from 'worker_threads'; import { inTest } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import type { EventMessageTypes } from '../event-message-classes'; import { isEventMessageOptions } from '../event-message-classes/abstract-event-message'; diff --git a/packages/cli/src/eventbus/message-event-bus/message-event-bus.ts b/packages/cli/src/eventbus/message-event-bus/message-event-bus.ts index 3cf5a5a5d0..2cd35a596f 100644 --- a/packages/cli/src/eventbus/message-event-bus/message-event-bus.ts +++ b/packages/cli/src/eventbus/message-event-bus/message-event-bus.ts @@ -5,6 +5,7 @@ import type { DeleteResult } from '@n8n/typeorm'; import { In } from '@n8n/typeorm'; import EventEmitter from 'events'; import uniqby from 'lodash/uniqBy'; +import { Logger } from 'n8n-core'; import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -13,7 +14,6 @@ import { EventDestinationsRepository } from '@/databases/repositories/event-dest import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import { ExecutionRecoveryService } from '../../executions/execution-recovery.service'; diff --git a/packages/cli/src/execution-lifecycle-hooks/__tests__/save-execution-progress.test.ts b/packages/cli/src/execution-lifecycle-hooks/__tests__/save-execution-progress.test.ts index eedbf27c9e..ac52cf3920 100644 --- a/packages/cli/src/execution-lifecycle-hooks/__tests__/save-execution-progress.test.ts +++ b/packages/cli/src/execution-lifecycle-hooks/__tests__/save-execution-progress.test.ts @@ -1,11 +1,11 @@ import { ErrorReporter } from 'n8n-core'; +import { Logger } from 'n8n-core'; import type { IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { saveExecutionProgress } from '@/execution-lifecycle-hooks/save-execution-progress'; import * as fnModule from '@/execution-lifecycle-hooks/to-save-settings'; import type { IExecutionResponse } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { mockInstance } from '@test/mocking'; mockInstance(Logger); diff --git a/packages/cli/src/execution-lifecycle-hooks/restore-binary-data-id.ts b/packages/cli/src/execution-lifecycle-hooks/restore-binary-data-id.ts index d9a1a9a0e9..15ac8b905c 100644 --- a/packages/cli/src/execution-lifecycle-hooks/restore-binary-data-id.ts +++ b/packages/cli/src/execution-lifecycle-hooks/restore-binary-data-id.ts @@ -1,10 +1,9 @@ -import { BinaryDataService } from 'n8n-core'; import type { BinaryData } from 'n8n-core'; +import { BinaryDataService, Logger } from 'n8n-core'; import type { IRun, WorkflowExecuteMode } from 'n8n-workflow'; import Container from 'typedi'; import config from '@/config'; -import { Logger } from '@/logging/logger.service'; /** * Whenever the execution ID is not available to the binary data service at the diff --git a/packages/cli/src/execution-lifecycle-hooks/save-execution-progress.ts b/packages/cli/src/execution-lifecycle-hooks/save-execution-progress.ts index c1de2646c0..2047c9e82e 100644 --- a/packages/cli/src/execution-lifecycle-hooks/save-execution-progress.ts +++ b/packages/cli/src/execution-lifecycle-hooks/save-execution-progress.ts @@ -1,10 +1,9 @@ -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import type { IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow'; import { Container } from 'typedi'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { toSaveSettings } from '@/execution-lifecycle-hooks/to-save-settings'; -import { Logger } from '@/logging/logger.service'; export async function saveExecutionProgress( workflowData: IWorkflowBase, diff --git a/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts b/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts index 68fd528f14..4c91222126 100644 --- a/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts +++ b/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts @@ -1,10 +1,10 @@ import pick from 'lodash/pick'; +import { Logger } from 'n8n-core'; import { ensureError, type ExecutionStatus, type IRun, type IWorkflowBase } from 'n8n-workflow'; import { Container } from 'typedi'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import type { IExecutionDb, UpdateExecutionPayload } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { ExecutionMetadataService } from '@/services/execution-metadata.service'; import { isWorkflowIdValid } from '@/utils'; diff --git a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts index 115a1a52f6..0e6017a2bd 100644 --- a/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution-recovery.service.test.ts @@ -22,7 +22,7 @@ import { setupMessages } from './utils'; describe('ExecutionRecoveryService', () => { const push = mockInstance(Push); - const instanceSettings = new InstanceSettings(mock()); + const instanceSettings = Container.get(InstanceSettings); let executionRecoveryService: ExecutionRecoveryService; let executionRepository: ExecutionRepository; diff --git a/packages/cli/src/executions/execution-recovery.service.ts b/packages/cli/src/executions/execution-recovery.service.ts index a10fc995a4..f307ce0677 100644 --- a/packages/cli/src/executions/execution-recovery.service.ts +++ b/packages/cli/src/executions/execution-recovery.service.ts @@ -1,5 +1,5 @@ import type { DateTime } from 'luxon'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { sleep } from 'n8n-workflow'; import type { IRun, ITaskData } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -10,7 +10,6 @@ import { NodeCrashedError } from '@/errors/node-crashed.error'; import { WorkflowCrashedError } from '@/errors/workflow-crashed.error'; import { EventService } from '@/events/event.service'; import type { IExecutionResponse } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { Push } from '@/push'; import { getWorkflowHooksMain } from '@/workflow-execute-additional-data'; // @TODO: Dependency cycle diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 433955254f..ffddb27164 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -1,5 +1,6 @@ import { GlobalConfig } from '@n8n/config'; import { validate as jsonSchemaValidate } from 'jsonschema'; +import { Logger } from 'n8n-core'; import type { ExecutionError, ExecutionStatus, @@ -38,7 +39,6 @@ import type { IWorkflowDb, } from '@/interfaces'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import { WaitTracker } from '@/wait-tracker'; import { WorkflowRunner } from '@/workflow-runner'; diff --git a/packages/cli/src/external-secrets/external-secrets-manager.ee.ts b/packages/cli/src/external-secrets/external-secrets-manager.ee.ts index 2de681a7d6..d4adf17255 100644 --- a/packages/cli/src/external-secrets/external-secrets-manager.ee.ts +++ b/packages/cli/src/external-secrets/external-secrets-manager.ee.ts @@ -1,4 +1,4 @@ -import { Cipher } from 'n8n-core'; +import { Cipher, Logger } from 'n8n-core'; import { jsonParse, type IDataObject, ApplicationError, ensureError } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -10,7 +10,6 @@ import type { SecretsProviderSettings, } from '@/interfaces'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import { EXTERNAL_SECRETS_INITIAL_BACKOFF, EXTERNAL_SECRETS_MAX_BACKOFF } from './constants'; diff --git a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts b/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts index 6c2c0669fb..629b3ad626 100644 --- a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts +++ b/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts @@ -1,10 +1,10 @@ +import { Logger } from 'n8n-core'; import type { INodeProperties } from 'n8n-workflow'; import Container from 'typedi'; import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { AwsSecretsClient } from './aws-secrets-client'; import type { AwsSecretsManagerContext } from './types'; diff --git a/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts b/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts index 7961f21bad..01995d6990 100644 --- a/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts +++ b/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts @@ -1,11 +1,11 @@ import type { SecretClient } from '@azure/keyvault-secrets'; +import { Logger } from 'n8n-core'; import { ensureError } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow'; import Container from 'typedi'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import type { AzureKeyVaultContext } from './types'; diff --git a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts b/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts index c4bf71cb72..ec29a28198 100644 --- a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts +++ b/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts @@ -1,10 +1,10 @@ import type { SecretManagerServiceClient as GcpClient } from '@google-cloud/secret-manager'; +import { Logger } from 'n8n-core'; import { ensureError, jsonParse, type INodeProperties } from 'n8n-workflow'; import Container from 'typedi'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import type { GcpSecretsManagerContext, diff --git a/packages/cli/src/external-secrets/providers/vault.ts b/packages/cli/src/external-secrets/providers/vault.ts index 0f1e93a5da..8030832376 100644 --- a/packages/cli/src/external-secrets/providers/vault.ts +++ b/packages/cli/src/external-secrets/providers/vault.ts @@ -1,11 +1,11 @@ import type { AxiosInstance, AxiosResponse } from 'axios'; import axios from 'axios'; +import { Logger } from 'n8n-core'; import type { IDataObject, INodeProperties } from 'n8n-workflow'; import { Container } from 'typedi'; import type { SecretsProviderSettings, SecretsProviderState } from '@/interfaces'; import { SecretsProvider } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '../constants'; import { preferGet } from '../external-secrets-helper.ee'; diff --git a/packages/cli/src/help.ts b/packages/cli/src/help.ts index 8581483577..168b9c9079 100644 --- a/packages/cli/src/help.ts +++ b/packages/cli/src/help.ts @@ -1,6 +1,6 @@ import { Help } from '@oclif/core'; -import Container from 'typedi'; -import { Logger } from 'winston'; +import { Logger } from 'n8n-core'; +import { Container } from 'typedi'; // oclif expects a default export // eslint-disable-next-line import/no-default-export diff --git a/packages/cli/src/ldap/ldap.service.ee.ts b/packages/cli/src/ldap/ldap.service.ee.ts index b552db6974..1445f56f13 100644 --- a/packages/cli/src/ldap/ldap.service.ee.ts +++ b/packages/cli/src/ldap/ldap.service.ee.ts @@ -2,7 +2,7 @@ import { QueryFailedError } from '@n8n/typeorm'; import type { Entry as LdapUser, ClientOptions } from 'ldapts'; import { Client } from 'ldapts'; -import { Cipher } from 'n8n-core'; +import { Cipher, Logger } from 'n8n-core'; import { ApplicationError, jsonParse } from 'n8n-workflow'; import type { ConnectionOptions } from 'tls'; import { Service } from 'typedi'; @@ -14,7 +14,6 @@ import { SettingsRepository } from '@/databases/repositories/settings.repository import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { EventService } from '@/events/event.service'; -import { Logger } from '@/logging/logger.service'; import { getCurrentAuthenticationMethod, isEmailCurrentAuthenticationMethod, diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index 2a3ae6fd6d..59804b6a7d 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -1,13 +1,12 @@ import { GlobalConfig } from '@n8n/config'; import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk'; import { LicenseManager } from '@n8n_io/license-sdk'; -import { InstanceSettings, ObjectStoreService } from 'n8n-core'; +import { InstanceSettings, ObjectStoreService, Logger } from 'n8n-core'; import Container, { Service } from 'typedi'; import config from '@/config'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { OnShutdown } from '@/decorators/on-shutdown'; -import { Logger } from '@/logging/logger.service'; import { LicenseMetricsService } from '@/metrics/license-metrics.service'; import { diff --git a/packages/cli/src/license/license.service.ts b/packages/cli/src/license/license.service.ts index 1419d58b83..750b42b432 100644 --- a/packages/cli/src/license/license.service.ts +++ b/packages/cli/src/license/license.service.ts @@ -1,4 +1,5 @@ import axios, { AxiosError } from 'axios'; +import { Logger } from 'n8n-core'; import { ensureError } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -7,7 +8,6 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { UrlService } from '@/services/url.service'; type LicenseError = Error & { errorId?: keyof typeof LicenseErrors }; diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index db62e8415e..e462bd4157 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -11,6 +11,7 @@ import { LazyPackageDirectoryLoader, UnrecognizedCredentialTypeError, UnrecognizedNodeTypeError, + Logger, } from 'n8n-core'; import type { KnownNodesAndCredentials, @@ -36,7 +37,6 @@ import { CLI_DIR, inE2ETests, } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import { isContainedWithin } from '@/utils/path-util'; interface LoadedNodesAndCredentials { diff --git a/packages/cli/src/logging/constants.ts b/packages/cli/src/logging/constants.ts deleted file mode 100644 index 107327694b..0000000000 --- a/packages/cli/src/logging/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const noOp = () => {}; - -export const LOG_LEVELS = ['error', 'warn', 'info', 'debug', 'silent'] as const; diff --git a/packages/cli/src/logging/types.ts b/packages/cli/src/logging/types.ts deleted file mode 100644 index bb01834326..0000000000 --- a/packages/cli/src/logging/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { LogScope } from '@n8n/config'; - -import type { LOG_LEVELS } from './constants'; - -export type LogLevel = (typeof LOG_LEVELS)[number]; - -export type LogMetadata = { - [key: string]: unknown; - scopes?: LogScope[]; - file?: string; - function?: string; -}; - -export type LogLocationMetadata = Pick; diff --git a/packages/cli/src/manual-execution.service.ts b/packages/cli/src/manual-execution.service.ts index 65174d20b5..c6749696fe 100644 --- a/packages/cli/src/manual-execution.service.ts +++ b/packages/cli/src/manual-execution.service.ts @@ -4,6 +4,7 @@ import { filterDisabledNodes, recreateNodeExecutionStack, WorkflowExecute, + Logger, } from 'n8n-core'; import type { IPinData, @@ -16,8 +17,6 @@ import type { import type PCancelable from 'p-cancelable'; import { Service } from 'typedi'; -import { Logger } from '@/logging/logger.service'; - @Service() export class ManualExecutionService { constructor(private readonly logger: Logger) {} diff --git a/packages/cli/src/push/__tests__/websocket.push.test.ts b/packages/cli/src/push/__tests__/websocket.push.test.ts index fd1e2f27a0..c13a319ebf 100644 --- a/packages/cli/src/push/__tests__/websocket.push.test.ts +++ b/packages/cli/src/push/__tests__/websocket.push.test.ts @@ -1,10 +1,10 @@ import type { PushMessage } from '@n8n/api-types'; import { EventEmitter } from 'events'; +import { Logger } from 'n8n-core'; import { Container } from 'typedi'; import type WebSocket from 'ws'; import type { User } from '@/databases/entities/user'; -import { Logger } from '@/logging/logger.service'; import { WebSocketPush } from '@/push/websocket.push'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/push/abstract.push.ts b/packages/cli/src/push/abstract.push.ts index 574f8a0def..b1c4514d8d 100644 --- a/packages/cli/src/push/abstract.push.ts +++ b/packages/cli/src/push/abstract.push.ts @@ -1,10 +1,9 @@ import type { PushMessage } from '@n8n/api-types'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import { assert, jsonStringify } from 'n8n-workflow'; import { Service } from 'typedi'; import type { User } from '@/databases/entities/user'; -import { Logger } from '@/logging/logger.service'; import type { OnPushMessage } from '@/push/types'; import { TypedEmitter } from '@/typed-emitter'; diff --git a/packages/cli/src/response-helper.ts b/packages/cli/src/response-helper.ts index 0e70aa312f..f7f448cc9d 100644 --- a/packages/cli/src/response-helper.ts +++ b/packages/cli/src/response-helper.ts @@ -1,13 +1,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import type { Request, Response } from 'express'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import { FORM_TRIGGER_PATH_IDENTIFIER, NodeApiError } from 'n8n-workflow'; import { Readable } from 'node:stream'; import picocolors from 'picocolors'; import Container from 'typedi'; import { inDevelopment } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import { ResponseError } from './errors/response-errors/abstract/response.error'; diff --git a/packages/cli/src/runners/__tests__/task-runner-process-restart-loop-detector.test.ts b/packages/cli/src/runners/__tests__/task-runner-process-restart-loop-detector.test.ts index 61cfb8b8e8..31abac807a 100644 --- a/packages/cli/src/runners/__tests__/task-runner-process-restart-loop-detector.test.ts +++ b/packages/cli/src/runners/__tests__/task-runner-process-restart-loop-detector.test.ts @@ -1,7 +1,7 @@ import { TaskRunnersConfig } from '@n8n/config'; import { mock } from 'jest-mock-extended'; +import type { Logger } from 'n8n-core'; -import type { Logger } from '@/logging/logger.service'; import type { TaskRunnerAuthService } from '@/runners/auth/task-runner-auth.service'; import { TaskRunnerRestartLoopError } from '@/runners/errors/task-runner-restart-loop-error'; import { RunnerLifecycleEvents } from '@/runners/runner-lifecycle-events'; diff --git a/packages/cli/src/runners/__tests__/task-runner-process.test.ts b/packages/cli/src/runners/__tests__/task-runner-process.test.ts index 85dbaa6930..42d6794287 100644 --- a/packages/cli/src/runners/__tests__/task-runner-process.test.ts +++ b/packages/cli/src/runners/__tests__/task-runner-process.test.ts @@ -1,8 +1,8 @@ import { TaskRunnersConfig } from '@n8n/config'; import { mock } from 'jest-mock-extended'; +import { Logger } from 'n8n-core'; import type { ChildProcess, SpawnOptions } from 'node:child_process'; -import { Logger } from '@/logging/logger.service'; import type { TaskRunnerAuthService } from '@/runners/auth/task-runner-auth.service'; import { TaskRunnerProcess } from '@/runners/task-runner-process'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/runners/runner-ws-server.ts b/packages/cli/src/runners/runner-ws-server.ts index 8ea3a7edbe..53b8f18cf8 100644 --- a/packages/cli/src/runners/runner-ws-server.ts +++ b/packages/cli/src/runners/runner-ws-server.ts @@ -1,11 +1,11 @@ import { TaskRunnersConfig } from '@n8n/config'; import type { BrokerMessage, RunnerMessage } from '@n8n/task-runner'; +import { Logger } from 'n8n-core'; import { ApplicationError, jsonStringify } from 'n8n-workflow'; import { Service } from 'typedi'; import type WebSocket from 'ws'; import { Time, WsStatusCodes } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import { DefaultTaskRunnerDisconnectAnalyzer } from './default-task-runner-disconnect-analyzer'; import { RunnerLifecycleEvents } from './runner-lifecycle-events'; diff --git a/packages/cli/src/runners/task-broker.service.ts b/packages/cli/src/runners/task-broker.service.ts index e52992d38e..ebbed80860 100644 --- a/packages/cli/src/runners/task-broker.service.ts +++ b/packages/cli/src/runners/task-broker.service.ts @@ -5,13 +5,13 @@ import type { RunnerMessage, TaskResultData, } from '@n8n/task-runner'; +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import { nanoid } from 'nanoid'; import { Service } from 'typedi'; import config from '@/config'; import { Time } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import { TaskDeferredError, TaskRejectError } from './errors'; import { TaskRunnerTimeoutError } from './errors/task-runner-timeout.error'; diff --git a/packages/cli/src/runners/task-runner-module.ts b/packages/cli/src/runners/task-runner-module.ts index 434daa066a..97f3f4e481 100644 --- a/packages/cli/src/runners/task-runner-module.ts +++ b/packages/cli/src/runners/task-runner-module.ts @@ -1,11 +1,10 @@ import { TaskRunnersConfig } from '@n8n/config'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import { sleep } from 'n8n-workflow'; import * as a from 'node:assert/strict'; import Container, { Service } from 'typedi'; import { OnShutdown } from '@/decorators/on-shutdown'; -import { Logger } from '@/logging/logger.service'; import type { TaskRunnerRestartLoopError } from '@/runners/errors/task-runner-restart-loop-error'; import type { TaskRunnerProcess } from '@/runners/task-runner-process'; import { TaskRunnerProcessRestartLoopDetector } from '@/runners/task-runner-process-restart-loop-detector'; diff --git a/packages/cli/src/runners/task-runner-process.ts b/packages/cli/src/runners/task-runner-process.ts index 2716383f17..6687929942 100644 --- a/packages/cli/src/runners/task-runner-process.ts +++ b/packages/cli/src/runners/task-runner-process.ts @@ -1,11 +1,11 @@ import { TaskRunnersConfig } from '@n8n/config'; +import { Logger } from 'n8n-core'; import * as a from 'node:assert/strict'; import { spawn } from 'node:child_process'; import * as process from 'node:process'; import { Service } from 'typedi'; import { OnShutdown } from '@/decorators/on-shutdown'; -import { Logger } from '@/logging/logger.service'; import { TaskRunnerAuthService } from './auth/task-runner-auth.service'; import { forwardToLogger } from './forward-to-logger'; diff --git a/packages/cli/src/runners/task-runner-server.ts b/packages/cli/src/runners/task-runner-server.ts index 2b1f481b0e..bbe1d7cd6a 100644 --- a/packages/cli/src/runners/task-runner-server.ts +++ b/packages/cli/src/runners/task-runner-server.ts @@ -1,6 +1,7 @@ import { GlobalConfig } from '@n8n/config'; import compression from 'compression'; import express from 'express'; +import { Logger } from 'n8n-core'; import * as a from 'node:assert/strict'; import { randomBytes } from 'node:crypto'; import { ServerResponse, type Server, createServer as createHttpServer } from 'node:http'; @@ -10,7 +11,6 @@ import { Service } from 'typedi'; import { Server as WSServer } from 'ws'; import { inTest } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import { bodyParser, rawBodyReader } from '@/middlewares'; import { send } from '@/response-helper'; import { TaskRunnerAuthController } from '@/runners/auth/task-runner-auth.controller'; diff --git a/packages/cli/src/scaling/job-processor.ts b/packages/cli/src/scaling/job-processor.ts index 51b86c3922..5e760e40c1 100644 --- a/packages/cli/src/scaling/job-processor.ts +++ b/packages/cli/src/scaling/job-processor.ts @@ -1,5 +1,5 @@ import type { RunningJobSummary } from '@n8n/api-types'; -import { ErrorReporter, InstanceSettings, WorkflowExecute } from 'n8n-core'; +import { ErrorReporter, InstanceSettings, WorkflowExecute, Logger } from 'n8n-core'; import type { ExecutionStatus, IExecuteResponsePromiseData, IRun } from 'n8n-workflow'; import { BINARY_ENCODING, ApplicationError, Workflow } from 'n8n-workflow'; import type PCancelable from 'p-cancelable'; @@ -8,7 +8,6 @@ import { Service } from 'typedi'; import config from '@/config'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; diff --git a/packages/cli/src/scaling/multi-main-setup.ee.ts b/packages/cli/src/scaling/multi-main-setup.ee.ts index dab9f17cc6..1a2554965c 100644 --- a/packages/cli/src/scaling/multi-main-setup.ee.ts +++ b/packages/cli/src/scaling/multi-main-setup.ee.ts @@ -1,10 +1,9 @@ import { GlobalConfig } from '@n8n/config'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { Service } from 'typedi'; import config from '@/config'; import { Time } from '@/constants'; -import { Logger } from '@/logging/logger.service'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import { RedisClientService } from '@/services/redis-client.service'; import { TypedEmitter } from '@/typed-emitter'; diff --git a/packages/cli/src/scaling/pubsub/publisher.service.ts b/packages/cli/src/scaling/pubsub/publisher.service.ts index 4723b1d37d..551d1b6caf 100644 --- a/packages/cli/src/scaling/pubsub/publisher.service.ts +++ b/packages/cli/src/scaling/pubsub/publisher.service.ts @@ -1,10 +1,9 @@ import type { Redis as SingleNodeClient, Cluster as MultiNodeClient } from 'ioredis'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; +import type { LogMetadata } from 'n8n-workflow'; import { Service } from 'typedi'; import config from '@/config'; -import { Logger } from '@/logging/logger.service'; -import type { LogMetadata } from '@/logging/types'; import { RedisClientService } from '@/services/redis-client.service'; import type { PubSub } from './pubsub.types'; diff --git a/packages/cli/src/scaling/pubsub/subscriber.service.ts b/packages/cli/src/scaling/pubsub/subscriber.service.ts index 0ce343c139..c2d5498243 100644 --- a/packages/cli/src/scaling/pubsub/subscriber.service.ts +++ b/packages/cli/src/scaling/pubsub/subscriber.service.ts @@ -1,13 +1,12 @@ import type { Redis as SingleNodeClient, Cluster as MultiNodeClient } from 'ioredis'; import debounce from 'lodash/debounce'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { jsonParse } from 'n8n-workflow'; +import type { LogMetadata } from 'n8n-workflow'; import { Service } from 'typedi'; import config from '@/config'; import { EventService } from '@/events/event.service'; -import { Logger } from '@/logging/logger.service'; -import type { LogMetadata } from '@/logging/types'; import { RedisClientService } from '@/services/redis-client.service'; import type { PubSub } from './pubsub.types'; diff --git a/packages/cli/src/scaling/scaling.service.ts b/packages/cli/src/scaling/scaling.service.ts index ebc8e4499c..5896e2976b 100644 --- a/packages/cli/src/scaling/scaling.service.ts +++ b/packages/cli/src/scaling/scaling.service.ts @@ -1,5 +1,5 @@ import { GlobalConfig } from '@n8n/config'; -import { ErrorReporter, InstanceSettings } from 'n8n-core'; +import { ErrorReporter, InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, BINARY_ENCODING, sleep, jsonStringify, ensureError } from 'n8n-workflow'; import type { IExecuteResponsePromiseData } from 'n8n-workflow'; import { strict } from 'node:assert'; @@ -12,7 +12,6 @@ import { ExecutionRepository } from '@/databases/repositories/execution.reposito import { OnShutdown } from '@/decorators/on-shutdown'; import { MaxStalledCountError } from '@/errors/max-stalled-count.error'; import { EventService } from '@/events/event.service'; -import { Logger } from '@/logging/logger.service'; import { OrchestrationService } from '@/services/orchestration.service'; import { assertNever } from '@/utils'; diff --git a/packages/cli/src/scaling/worker-server.ts b/packages/cli/src/scaling/worker-server.ts index ee622d789c..8112f3a4f5 100644 --- a/packages/cli/src/scaling/worker-server.ts +++ b/packages/cli/src/scaling/worker-server.ts @@ -1,7 +1,7 @@ import { GlobalConfig } from '@n8n/config'; import type { Application } from 'express'; import express from 'express'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { strict as assert } from 'node:assert'; import http from 'node:http'; import type { Server } from 'node:http'; @@ -13,7 +13,6 @@ import { CredentialsOverwritesAlreadySetError } from '@/errors/credentials-overw import { NonJsonBodyError } from '@/errors/non-json-body.error'; import { ExternalHooks } from '@/external-hooks'; import type { ICredentialsOverwrite } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { PrometheusMetricsService } from '@/metrics/prometheus-metrics.service'; import { rawBodyReader, bodyParser } from '@/middlewares'; import * as ResponseHelper from '@/response-helper'; diff --git a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts index 37113e4c40..92874e87f0 100644 --- a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts +++ b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts @@ -1,12 +1,11 @@ import { GlobalConfig } from '@n8n/config'; import axios from 'axios'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { Service } from 'typedi'; import config from '@/config'; import { getN8nPackageJson, inDevelopment } from '@/constants'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; -import { Logger } from '@/logging/logger.service'; import { isApiEnabled } from '@/public-api'; import { ENV_VARS_DOCS_URL, diff --git a/packages/cli/src/services/active-workflows.service.ts b/packages/cli/src/services/active-workflows.service.ts index 61aa875d1a..d2c50c74c2 100644 --- a/packages/cli/src/services/active-workflows.service.ts +++ b/packages/cli/src/services/active-workflows.service.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import { Service } from 'typedi'; import { ActivationErrorsService } from '@/activation-errors.service'; @@ -5,7 +6,6 @@ import type { User } from '@/databases/entities/user'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { Logger } from '@/logging/logger.service'; @Service() export class ActiveWorkflowsService { diff --git a/packages/cli/src/services/community-packages.service.ts b/packages/cli/src/services/community-packages.service.ts index 9f09d0c310..a94b4d5c4f 100644 --- a/packages/cli/src/services/community-packages.service.ts +++ b/packages/cli/src/services/community-packages.service.ts @@ -2,8 +2,8 @@ import { GlobalConfig } from '@n8n/config'; import axios from 'axios'; import { exec } from 'child_process'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; -import { InstanceSettings } from 'n8n-core'; import type { PackageDirectoryLoader } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, type PublicInstalledPackage } from 'n8n-workflow'; import { Service } from 'typedi'; import { promisify } from 'util'; @@ -22,7 +22,6 @@ import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import type { CommunityPackages } from '@/interfaces'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; -import { Logger } from '@/logging/logger.service'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import { toError } from '@/utils'; diff --git a/packages/cli/src/services/credentials-tester.service.ts b/packages/cli/src/services/credentials-tester.service.ts index 6ae7201ac0..0709d77e06 100644 --- a/packages/cli/src/services/credentials-tester.service.ts +++ b/packages/cli/src/services/credentials-tester.service.ts @@ -4,7 +4,13 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ import get from 'lodash/get'; -import { ErrorReporter, NodeExecuteFunctions, RoutingNode, isObjectLiteral } from 'n8n-core'; +import { + ErrorReporter, + Logger, + NodeExecuteFunctions, + RoutingNode, + isObjectLiteral, +} from 'n8n-core'; import type { ICredentialsDecrypted, ICredentialTestFunction, @@ -28,7 +34,6 @@ import { Service } from 'typedi'; import { CredentialTypes } from '@/credential-types'; import type { User } from '@/databases/entities/user'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 1645e98304..75fe36358f 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -3,7 +3,7 @@ import { GlobalConfig, FrontendConfig, SecurityConfig } from '@n8n/config'; import { createWriteStream } from 'fs'; import { mkdir } from 'fs/promises'; import uniq from 'lodash/uniq'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import type { ICredentialType, INodeTypeBaseDescription } from 'n8n-workflow'; import path from 'path'; import { Container, Service } from 'typedi'; @@ -16,7 +16,6 @@ import { getVariablesLimit } from '@/environments/variables/environment-helpers' import { getLdapLoginLabel } from '@/ldap/helpers.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; -import { Logger } from '@/logging/logger.service'; import { isApiEnabled } from '@/public-api'; import type { CommunityPackagesService } from '@/services/community-packages.service'; import { getSamlLoginLabel } from '@/sso/saml/saml-helpers'; diff --git a/packages/cli/src/services/import.service.ts b/packages/cli/src/services/import.service.ts index 2402863bab..4f63357dd6 100644 --- a/packages/cli/src/services/import.service.ts +++ b/packages/cli/src/services/import.service.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import { type INode, type INodeCredentialsDetails } from 'n8n-workflow'; import { Service } from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -11,7 +12,6 @@ import { CredentialsRepository } from '@/databases/repositories/credentials.repo import { TagRepository } from '@/databases/repositories/tag.repository'; import * as Db from '@/db'; import type { ICredentialsDb } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { replaceInvalidCredentials } from '@/workflow-helpers'; @Service() diff --git a/packages/cli/src/services/pruning/pruning.service.ts b/packages/cli/src/services/pruning/pruning.service.ts index aad8c5490f..15d81dc48e 100644 --- a/packages/cli/src/services/pruning/pruning.service.ts +++ b/packages/cli/src/services/pruning/pruning.service.ts @@ -1,5 +1,5 @@ import { ExecutionsConfig } from '@n8n/config'; -import { BinaryDataService, InstanceSettings } from 'n8n-core'; +import { BinaryDataService, InstanceSettings, Logger } from 'n8n-core'; import { ensureError } from 'n8n-workflow'; import { strict } from 'node:assert'; import { Service } from 'typedi'; @@ -8,7 +8,6 @@ import { Time } from '@/constants'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { connectionState as dbConnectionState } from '@/db'; import { OnShutdown } from '@/decorators/on-shutdown'; -import { Logger } from '@/logging/logger.service'; import { OrchestrationService } from '../orchestration.service'; diff --git a/packages/cli/src/services/redis-client.service.ts b/packages/cli/src/services/redis-client.service.ts index c584530165..894d3af671 100644 --- a/packages/cli/src/services/redis-client.service.ts +++ b/packages/cli/src/services/redis-client.service.ts @@ -1,10 +1,10 @@ import { GlobalConfig } from '@n8n/config'; import ioRedis from 'ioredis'; import type { Cluster, RedisOptions } from 'ioredis'; +import { Logger } from 'n8n-core'; import { Service } from 'typedi'; import { Debounce } from '@/decorators/debounce'; -import { Logger } from '@/logging/logger.service'; import { TypedEmitter } from '@/typed-emitter'; import type { RedisClientType } from '../scaling/redis/redis.types'; diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index e47dd026b0..3d2cb30471 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import type { IUserSettings } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -7,7 +8,6 @@ import { UserRepository } from '@/databases/repositories/user.repository'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { EventService } from '@/events/event.service'; import type { Invitation, PublicUser } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import type { PostHogClient } from '@/posthog'; import type { UserRequest } from '@/requests'; import { UrlService } from '@/services/url.service'; diff --git a/packages/cli/src/services/workflow-statistics.service.ts b/packages/cli/src/services/workflow-statistics.service.ts index 53cbac5094..9c0282eb85 100644 --- a/packages/cli/src/services/workflow-statistics.service.ts +++ b/packages/cli/src/services/workflow-statistics.service.ts @@ -1,10 +1,10 @@ +import { Logger } from 'n8n-core'; import type { INode, IRun, IWorkflowBase } from 'n8n-workflow'; import { Service } from 'typedi'; import { StatisticsNames } from '@/databases/entities/workflow-statistics'; import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository'; import { EventService } from '@/events/event.service'; -import { Logger } from '@/logging/logger.service'; import { UserService } from '@/services/user.service'; import { TypedEmitter } from '@/typed-emitter'; diff --git a/packages/cli/src/shutdown/shutdown.service.ts b/packages/cli/src/shutdown/shutdown.service.ts index 8ff8570757..1597b2a27f 100644 --- a/packages/cli/src/shutdown/shutdown.service.ts +++ b/packages/cli/src/shutdown/shutdown.service.ts @@ -1,9 +1,9 @@ import { type Class, ErrorReporter } from 'n8n-core'; +import { Logger } from 'n8n-core'; import { ApplicationError, assert } from 'n8n-workflow'; import { Container, Service } from 'typedi'; import { LOWEST_SHUTDOWN_PRIORITY, HIGHEST_SHUTDOWN_PRIORITY } from '@/constants'; -import { Logger } from '@/logging/logger.service'; type HandlerFn = () => Promise | void; export type ServiceClass = Class>; diff --git a/packages/cli/src/sso/saml/__tests__/saml-validator.test.ts b/packages/cli/src/sso/saml/__tests__/saml-validator.test.ts index 8594676ab2..563c7934ea 100644 --- a/packages/cli/src/sso/saml/__tests__/saml-validator.test.ts +++ b/packages/cli/src/sso/saml/__tests__/saml-validator.test.ts @@ -1,4 +1,5 @@ -import { Logger } from '@/logging/logger.service'; +import { Logger } from 'n8n-core'; + import { mockInstance } from '@test/mocking'; import { validateMetadata, validateResponse } from '../saml-validator'; diff --git a/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts b/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts index 8bd5e32da2..708592e8e7 100644 --- a/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts +++ b/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts @@ -1,9 +1,9 @@ import type express from 'express'; import { mock } from 'jest-mock-extended'; +import { Logger } from 'n8n-core'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; -import { Logger } from '@/logging/logger.service'; import { UrlService } from '@/services/url.service'; import * as samlHelpers from '@/sso/saml/saml-helpers'; import { SamlService } from '@/sso/saml/saml.service.ee'; diff --git a/packages/cli/src/sso/saml/saml-validator.ts b/packages/cli/src/sso/saml/saml-validator.ts index 07e9853f90..582fe624a5 100644 --- a/packages/cli/src/sso/saml/saml-validator.ts +++ b/packages/cli/src/sso/saml/saml-validator.ts @@ -1,8 +1,7 @@ +import { Logger } from 'n8n-core'; import { Container } from 'typedi'; import type { XMLFileInfo } from 'xmllint-wasm'; -import { Logger } from '@/logging/logger.service'; - let xmlMetadata: XMLFileInfo; let xmlProtocol: XMLFileInfo; diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index 3672c8fe6f..2944d9adf1 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import type express from 'express'; import https from 'https'; +import { Logger } from 'n8n-core'; import { ApplicationError, jsonParse } from 'n8n-workflow'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity'; @@ -12,7 +13,6 @@ import { SettingsRepository } from '@/databases/repositories/settings.repository import { UserRepository } from '@/databases/repositories/user.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { Logger } from '@/logging/logger.service'; import { UrlService } from '@/services/url.service'; import { SAML_PREFERENCES_DB_KEY } from './constants'; diff --git a/packages/cli/src/subworkflows/subworkflow-policy-checker.service.ts b/packages/cli/src/subworkflows/subworkflow-policy-checker.service.ts index 6c64fc0b3a..0415977e2e 100644 --- a/packages/cli/src/subworkflows/subworkflow-policy-checker.service.ts +++ b/packages/cli/src/subworkflows/subworkflow-policy-checker.service.ts @@ -1,10 +1,10 @@ import { GlobalConfig } from '@n8n/config'; +import { Logger } from 'n8n-core'; import { type Workflow, type INode, type WorkflowSettings } from 'n8n-workflow'; import { Service } from 'typedi'; import type { Project } from '@/databases/entities/project'; import { SubworkflowPolicyDenialError } from '@/errors/subworkflow-policy-denial.error'; -import { Logger } from '@/logging/logger.service'; import { AccessService } from '@/services/access.service'; import { OwnershipService } from '@/services/ownership.service'; import { UrlService } from '@/services/url.service'; diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index a8d39d898e..e8099e32cb 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -1,7 +1,7 @@ import { GlobalConfig } from '@n8n/config'; import type RudderStack from '@rudderstack/rudder-sdk-node'; import axios from 'axios'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import type { ITelemetryTrackProperties } from 'n8n-workflow'; import { Container, Service } from 'typedi'; @@ -13,7 +13,6 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { OnShutdown } from '@/decorators/on-shutdown'; import type { IExecutionTrackProperties } from '@/interfaces'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { PostHogClient } from '@/posthog'; import { SourceControlPreferencesService } from '../environments/source-control/source-control-preferences.service.ee'; diff --git a/packages/cli/src/user-management/email/node-mailer.ts b/packages/cli/src/user-management/email/node-mailer.ts index a35ab77318..4f4dc4f895 100644 --- a/packages/cli/src/user-management/email/node-mailer.ts +++ b/packages/cli/src/user-management/email/node-mailer.ts @@ -1,14 +1,12 @@ import { GlobalConfig } from '@n8n/config'; import { pick } from 'lodash'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import path from 'node:path'; import type { Transporter } from 'nodemailer'; import { createTransport } from 'nodemailer'; import type SMTPConnection from 'nodemailer/lib/smtp-connection'; import { Service } from 'typedi'; -import { Logger } from '@/logging/logger.service'; - import type { MailData, SendEmailResult } from './interfaces'; @Service() diff --git a/packages/cli/src/user-management/email/user-management-mailer.ts b/packages/cli/src/user-management/email/user-management-mailer.ts index 3acddad185..25794cec8d 100644 --- a/packages/cli/src/user-management/email/user-management-mailer.ts +++ b/packages/cli/src/user-management/email/user-management-mailer.ts @@ -2,6 +2,7 @@ import { GlobalConfig } from '@n8n/config'; import { existsSync } from 'fs'; import { readFile } from 'fs/promises'; import Handlebars from 'handlebars'; +import { Logger } from 'n8n-core'; import { join as pathJoin } from 'path'; import { Container, Service } from 'typedi'; @@ -11,7 +12,6 @@ import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { UserRepository } from '@/databases/repositories/user.repository'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { EventService } from '@/events/event.service'; -import { Logger } from '@/logging/logger.service'; import { UrlService } from '@/services/url.service'; import { toError } from '@/utils'; diff --git a/packages/cli/src/wait-tracker.ts b/packages/cli/src/wait-tracker.ts index f42905ace1..02480110ae 100644 --- a/packages/cli/src/wait-tracker.ts +++ b/packages/cli/src/wait-tracker.ts @@ -1,10 +1,9 @@ -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, Logger } from 'n8n-core'; import { ApplicationError, type IWorkflowExecutionDataProcess } from 'n8n-workflow'; import { Service } from 'typedi'; import { ActiveExecutions } from '@/active-executions'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; -import { Logger } from '@/logging/logger.service'; import { OrchestrationService } from '@/services/orchestration.service'; import { OwnershipService } from '@/services/ownership.service'; import { WorkflowRunner } from '@/workflow-runner'; diff --git a/packages/cli/src/webhooks/live-webhooks.ts b/packages/cli/src/webhooks/live-webhooks.ts index 6d6fc9161d..1ebfef3470 100644 --- a/packages/cli/src/webhooks/live-webhooks.ts +++ b/packages/cli/src/webhooks/live-webhooks.ts @@ -1,4 +1,5 @@ import type { Response } from 'express'; +import { Logger } from 'n8n-core'; import { Workflow, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow'; import type { INode, IWebhookData, IHttpRequestMethods } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -6,7 +7,6 @@ import { Service } from 'typedi'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { WebhookNotFoundError } from '@/errors/response-errors/webhook-not-found.error'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import { WebhookService } from '@/webhooks/webhook.service'; diff --git a/packages/cli/src/webhooks/waiting-webhooks.ts b/packages/cli/src/webhooks/waiting-webhooks.ts index 6355709189..ed2f8404bc 100644 --- a/packages/cli/src/webhooks/waiting-webhooks.ts +++ b/packages/cli/src/webhooks/waiting-webhooks.ts @@ -1,4 +1,5 @@ import type express from 'express'; +import { Logger } from 'n8n-core'; import { FORM_NODE_TYPE, type INodes, @@ -13,7 +14,6 @@ import { ExecutionRepository } from '@/databases/repositories/execution.reposito import { ConflictError } from '@/errors/response-errors/conflict.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IExecutionResponse, IWorkflowDb } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; diff --git a/packages/cli/src/webhooks/webhook-helpers.ts b/packages/cli/src/webhooks/webhook-helpers.ts index 6657089881..51b81fad83 100644 --- a/packages/cli/src/webhooks/webhook-helpers.ts +++ b/packages/cli/src/webhooks/webhook-helpers.ts @@ -9,7 +9,7 @@ import { GlobalConfig } from '@n8n/config'; import type express from 'express'; import get from 'lodash/get'; -import { BinaryDataService, ErrorReporter } from 'n8n-core'; +import { BinaryDataService, ErrorReporter, Logger } from 'n8n-core'; import type { IBinaryData, IBinaryKeyData, @@ -46,7 +46,6 @@ import { InternalServerError } from '@/errors/response-errors/internal-server.er import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; import type { IWorkflowDb } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { parseBody } from '@/middlewares'; import { OwnershipService } from '@/services/ownership.service'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; diff --git a/packages/cli/src/webhooks/webhook.service.ts b/packages/cli/src/webhooks/webhook.service.ts index 80b12b04cd..f571c9450b 100644 --- a/packages/cli/src/webhooks/webhook.service.ts +++ b/packages/cli/src/webhooks/webhook.service.ts @@ -1,4 +1,4 @@ -import { HookContext, WebhookContext } from 'n8n-core'; +import { HookContext, WebhookContext, Logger } from 'n8n-core'; import { ApplicationError, Node, NodeHelpers } from 'n8n-workflow'; import type { IHttpRequestMethods, @@ -16,7 +16,6 @@ import { Service } from 'typedi'; import type { WebhookEntity } from '@/databases/entities/webhook-entity'; import { WebhookRepository } from '@/databases/repositories/webhook.repository'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import { CacheService } from '@/services/cache/cache.service'; diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 29c8d67502..bf97ce0a3f 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -5,7 +5,7 @@ import type { PushMessage, PushType } from '@n8n/api-types'; import { GlobalConfig } from '@n8n/config'; import { stringify } from 'flatted'; -import { ErrorReporter, WorkflowExecute, isObjectLiteral } from 'n8n-core'; +import { ErrorReporter, Logger, WorkflowExecute, isObjectLiteral } from 'n8n-core'; import { ApplicationError, NodeOperationError, Workflow, WorkflowHooks } from 'n8n-workflow'; import type { IDataObject, @@ -58,7 +58,6 @@ import { updateExistingExecution, } from './execution-lifecycle-hooks/shared/shared-hook-functions'; import { toSaveSettings } from './execution-lifecycle-hooks/to-save-settings'; -import { Logger } from './logging/logger.service'; import { TaskManager } from './runners/task-managers/task-manager'; import { SecretsHelper } from './secrets-helpers'; import { OwnershipService } from './services/ownership.service'; diff --git a/packages/cli/src/workflow-runner.ts b/packages/cli/src/workflow-runner.ts index 973d512e62..30cd50d6f0 100644 --- a/packages/cli/src/workflow-runner.ts +++ b/packages/cli/src/workflow-runner.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { ErrorReporter, InstanceSettings, WorkflowExecute } from 'n8n-core'; +import { ErrorReporter, InstanceSettings, Logger, WorkflowExecute } from 'n8n-core'; import type { ExecutionError, IDeferredPromise, @@ -21,7 +21,6 @@ import { ActiveExecutions } from '@/active-executions'; import config from '@/config'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { ExternalHooks } from '@/external-hooks'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import type { ScalingService } from '@/scaling/scaling.service'; import type { Job, JobData } from '@/scaling/scaling.types'; diff --git a/packages/cli/src/workflows/workflow-execution.service.ts b/packages/cli/src/workflows/workflow-execution.service.ts index 27b673c245..842ddfe726 100644 --- a/packages/cli/src/workflows/workflow-execution.service.ts +++ b/packages/cli/src/workflows/workflow-execution.service.ts @@ -1,5 +1,5 @@ import { GlobalConfig } from '@n8n/config'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import type { IDeferredPromise, IExecuteData, @@ -20,7 +20,6 @@ import type { User } from '@/databases/entities/user'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { CreateExecutionPayload, IWorkflowDb, IWorkflowErrorData } from '@/interfaces'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service'; import { TestWebhooks } from '@/webhooks/test-webhooks'; diff --git a/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts b/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts index 3b171e3422..2e23a7d64a 100644 --- a/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts +++ b/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts @@ -1,3 +1,4 @@ +import { Logger } from 'n8n-core'; import { ensureError } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -8,7 +9,6 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; -import { Logger } from '@/logging/logger.service'; import { isWorkflowHistoryEnabled } from './workflow-history-helper.ee'; diff --git a/packages/cli/src/workflows/workflow-static-data.service.ts b/packages/cli/src/workflows/workflow-static-data.service.ts index 3e5159dc9a..aaff18f319 100644 --- a/packages/cli/src/workflows/workflow-static-data.service.ts +++ b/packages/cli/src/workflows/workflow-static-data.service.ts @@ -1,10 +1,9 @@ import { GlobalConfig } from '@n8n/config'; -import { ErrorReporter } from 'n8n-core'; +import { ErrorReporter, Logger } from 'n8n-core'; import type { IDataObject, Workflow } from 'n8n-workflow'; import { Service } from 'typedi'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { Logger } from '@/logging/logger.service'; import { isWorkflowIdValid } from '@/utils'; @Service() diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 90a8af90b1..debaf85073 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { In, type EntityManager } from '@n8n/typeorm'; import omit from 'lodash/omit'; +import { Logger } from 'n8n-core'; import { ApplicationError, NodeOperationError, WorkflowActivationError } from 'n8n-workflow'; import { Service } from 'typedi'; @@ -17,7 +18,6 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { TransferWorkflowError } from '@/errors/response-errors/transfer-workflow.error'; -import { Logger } from '@/logging/logger.service'; import { OwnershipService } from '@/services/ownership.service'; import { ProjectService } from '@/services/project.service'; diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 7220e1a640..facd372656 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -5,7 +5,7 @@ import type { EntityManager } from '@n8n/typeorm'; import { In } from '@n8n/typeorm'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; -import { BinaryDataService } from 'n8n-core'; +import { BinaryDataService, Logger } from 'n8n-core'; import { NodeApiError } from 'n8n-workflow'; import { Service } from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -24,7 +24,6 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; -import { Logger } from '@/logging/logger.service'; import { hasSharing, type ListQuery } from '@/requests'; import { OrchestrationService } from '@/services/orchestration.service'; import { OwnershipService } from '@/services/ownership.service'; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 24765b422a..f097b3cab6 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -3,6 +3,7 @@ import { GlobalConfig } from '@n8n/config'; import { In, type FindOptionsRelations } from '@n8n/typeorm'; import axios from 'axios'; import express from 'express'; +import { Logger } from 'n8n-core'; import { ApplicationError } from 'n8n-workflow'; import { v4 as uuid } from 'uuid'; import { z } from 'zod'; @@ -27,7 +28,6 @@ import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import type { IWorkflowResponse } from '@/interfaces'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { listQueryMiddleware } from '@/middlewares'; import * as ResponseHelper from '@/response-helper'; import { NamingService } from '@/services/naming.service'; diff --git a/packages/cli/test/integration/active-workflow-manager.test.ts b/packages/cli/test/integration/active-workflow-manager.test.ts index a3e4f657f2..f724d156a8 100644 --- a/packages/cli/test/integration/active-workflow-manager.test.ts +++ b/packages/cli/test/integration/active-workflow-manager.test.ts @@ -1,4 +1,5 @@ import { mock } from 'jest-mock-extended'; +import { Logger } from 'n8n-core'; import { NodeApiError, Workflow } from 'n8n-workflow'; import type { IWebhookData, WorkflowActivateMode } from 'n8n-workflow'; import { Container } from 'typedi'; @@ -9,7 +10,6 @@ import type { WebhookEntity } from '@/databases/entities/webhook-entity'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { ExecutionService } from '@/executions/execution.service'; import { ExternalHooks } from '@/external-hooks'; -import { Logger } from '@/logging/logger.service'; import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; import { SecretsHelper } from '@/secrets-helpers'; diff --git a/packages/cli/test/integration/pruning.service.test.ts b/packages/cli/test/integration/pruning.service.test.ts index 4f34048a1a..9d4103e84e 100644 --- a/packages/cli/test/integration/pruning.service.test.ts +++ b/packages/cli/test/integration/pruning.service.test.ts @@ -21,7 +21,7 @@ import { mockInstance, mockLogger } from '../shared/mocking'; describe('softDeleteOnPruningCycle()', () => { let pruningService: PruningService; - const instanceSettings = new InstanceSettings(mock()); + const instanceSettings = Container.get(InstanceSettings); instanceSettings.markAsLeader(); const now = new Date(); diff --git a/packages/cli/test/integration/shared/db/workflows.ts b/packages/cli/test/integration/shared/db/workflows.ts index 5c86f1dc35..bc8099e494 100644 --- a/packages/cli/test/integration/shared/db/workflows.ts +++ b/packages/cli/test/integration/shared/db/workflows.ts @@ -40,6 +40,7 @@ export function newWorkflow(attributes: Partial = {}): WorkflowE ], connections: connections ?? {}, versionId: versionId ?? uuid(), + settings: {}, ...attributes, }); diff --git a/packages/cli/test/integration/shared/utils/test-server.ts b/packages/cli/test/integration/shared/utils/test-server.ts index ef0588b8d7..f99e093854 100644 --- a/packages/cli/test/integration/shared/utils/test-server.ts +++ b/packages/cli/test/integration/shared/utils/test-server.ts @@ -1,5 +1,6 @@ import cookieParser from 'cookie-parser'; import express from 'express'; +import { Logger } from 'n8n-core'; import type superagent from 'superagent'; import request from 'supertest'; import { Container } from 'typedi'; @@ -11,7 +12,6 @@ import { AUTH_COOKIE_NAME } from '@/constants'; import type { User } from '@/databases/entities/user'; import { ControllerRegistry } from '@/decorators'; import { License } from '@/license'; -import { Logger } from '@/logging/logger.service'; import { rawBodyReader, bodyParser } from '@/middlewares'; import { PostHogClient } from '@/posthog'; import { Push } from '@/push'; diff --git a/packages/cli/test/shared/mocking.ts b/packages/cli/test/shared/mocking.ts index 129acb585c..535388c556 100644 --- a/packages/cli/test/shared/mocking.ts +++ b/packages/cli/test/shared/mocking.ts @@ -1,11 +1,10 @@ import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm'; import { mock } from 'jest-mock-extended'; import type { Class } from 'n8n-core'; +import type { Logger } from 'n8n-core'; import type { DeepPartial } from 'ts-essentials'; import { Container } from 'typedi'; -import type { Logger } from '@/logging/logger.service'; - export const mockInstance = ( serviceClass: Class, data: DeepPartial | undefined = undefined, diff --git a/packages/core/package.json b/packages/core/package.json index 26b41800d9..401a9f81c7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ "@sentry/node": "catalog:", "aws4": "1.11.0", "axios": "catalog:", + "callsites": "catalog:", "chardet": "2.0.0", "concat-stream": "2.0.0", "cron": "3.1.7", @@ -56,11 +57,13 @@ "nanoid": "catalog:", "oauth-1.0a": "2.2.6", "p-cancelable": "2.1.1", + "picocolors": "catalog:", "pretty-bytes": "5.6.0", "qs": "6.11.0", "ssh2": "1.15.0", "typedi": "catalog:", "uuid": "catalog:", + "winston": "3.14.2", "xml2js": "catalog:", "zod": "catalog:" } diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index e3ca8614c2..173f73baca 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -11,7 +11,6 @@ import type { } from 'n8n-workflow'; import { ApplicationError, - LoggerProxy as Logger, toCronExpression, TriggerCloseError, WorkflowActivationError, @@ -21,12 +20,14 @@ import { Service } from 'typedi'; import { ErrorReporter } from './error-reporter'; import type { IWorkflowData } from './Interfaces'; +import { Logger } from './logging/logger'; import { ScheduledTaskManager } from './ScheduledTaskManager'; import { TriggersAndPollers } from './TriggersAndPollers'; @Service() export class ActiveWorkflows { constructor( + private readonly logger: Logger, private readonly scheduledTaskManager: ScheduledTaskManager, private readonly triggersAndPollers: TriggersAndPollers, private readonly errorReporter: ErrorReporter, @@ -151,7 +152,7 @@ export class ActiveWorkflows { const cronTimes = (pollTimes.item || []).map(toCronExpression); // The trigger function to execute when the cron-time got reached const executeTrigger = async (testingTrigger = false) => { - Logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, { + this.logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, { workflowName: workflow.name, workflowId: workflow.id, }); @@ -193,7 +194,7 @@ export class ActiveWorkflows { */ async remove(workflowId: string) { if (!this.isActive(workflowId)) { - Logger.warn(`Cannot deactivate already inactive workflow ID "${workflowId}"`); + this.logger.warn(`Cannot deactivate already inactive workflow ID "${workflowId}"`); return false; } @@ -222,7 +223,7 @@ export class ActiveWorkflows { await response.closeFunction(); } catch (e) { if (e instanceof TriggerCloseError) { - Logger.error( + this.logger.error( `There was a problem calling "closeFunction" on "${e.node.name}" in workflow "${workflowId}"`, ); this.errorReporter.error(e, { extra: { workflowId } }); diff --git a/packages/core/src/Constants.ts b/packages/core/src/Constants.ts index ceaf77566a..82a39b07cd 100644 --- a/packages/core/src/Constants.ts +++ b/packages/core/src/Constants.ts @@ -1,6 +1,10 @@ import type { INodeProperties } from 'n8n-workflow'; import { cronNodeOptions } from 'n8n-workflow'; +const { NODE_ENV } = process.env; +export const inProduction = NODE_ENV === 'production'; +export const inDevelopment = !NODE_ENV || NODE_ENV === 'development'; + export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS'; export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; diff --git a/packages/core/src/DirectoryLoader.ts b/packages/core/src/DirectoryLoader.ts index cd223da2dd..fe9fee8769 100644 --- a/packages/core/src/DirectoryLoader.ts +++ b/packages/core/src/DirectoryLoader.ts @@ -15,15 +15,13 @@ import type { IVersionedNodeType, KnownNodesAndCredentials, } from 'n8n-workflow'; -import { - ApplicationError, - LoggerProxy as Logger, - applyDeclarativeNodeOptionParameters, - jsonParse, -} from 'n8n-workflow'; +import { ApplicationError, applyDeclarativeNodeOptionParameters, jsonParse } from 'n8n-workflow'; import { readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import * as path from 'path'; +import Container from 'typedi'; + +import { Logger } from '@/logging/logger'; import { loadClassInIsolation } from './ClassLoader'; import { commonCORSParameters, commonPollingParameters, CUSTOM_NODES_CATEGORY } from './Constants'; @@ -78,6 +76,8 @@ export abstract class DirectoryLoader { readonly nodesByCredential: Record = {}; + protected readonly logger = Container.get(Logger); + constructor( readonly directory: string, protected readonly excludeNodes: string[] = [], @@ -336,7 +336,7 @@ export abstract class DirectoryLoader { node.description.codex = codex; } catch { - Logger.debug(`No codex available for: ${node.description.name}`); + this.logger.debug(`No codex available for: ${node.description.name}`); if (isCustom) { node.description.codex = { @@ -454,7 +454,7 @@ export class PackageDirectoryLoader extends DirectoryLoader { this.inferSupportedNodes(); - Logger.debug(`Loaded all credentials and nodes from ${this.packageName}`, { + this.logger.debug(`Loaded all credentials and nodes from ${this.packageName}`, { credentials: credentials?.length ?? 0, nodes: nodes?.length ?? 0, }); @@ -550,7 +550,7 @@ export class LazyPackageDirectoryLoader extends PackageDirectoryLoader { ); } - Logger.debug(`Lazy-loading nodes and credentials from ${this.packageJson.name}`, { + this.logger.debug(`Lazy-loading nodes and credentials from ${this.packageJson.name}`, { nodes: this.types.nodes?.length ?? 0, credentials: this.types.credentials?.length ?? 0, }); @@ -559,7 +559,7 @@ export class LazyPackageDirectoryLoader extends PackageDirectoryLoader { return; // We can load nodes and credentials lazily now } catch { - Logger.debug("Can't enable lazy-loading"); + this.logger.debug("Can't enable lazy-loading"); await super.loadAll(); } } diff --git a/packages/core/src/InstanceSettings.ts b/packages/core/src/InstanceSettings.ts index f611e034b3..814f75ef94 100644 --- a/packages/core/src/InstanceSettings.ts +++ b/packages/core/src/InstanceSettings.ts @@ -5,6 +5,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync import path from 'path'; import { Service } from 'typedi'; +import { Logger } from '@/logging/logger'; + import { Memoized } from './decorators'; import { InstanceSettingsConfig } from './InstanceSettingsConfig'; @@ -28,13 +30,11 @@ const inTest = process.env.NODE_ENV === 'test'; @Service() export class InstanceSettings { - private readonly userHome = this.getUserHome(); - /** The path to the n8n folder in which all n8n related data gets saved */ - readonly n8nFolder = path.join(this.userHome, '.n8n'); + readonly n8nFolder = this.config.n8nFolder; /** The path to the folder where all generated static assets are copied to */ - readonly staticCacheDir = path.join(this.userHome, '.cache/n8n/public'); + readonly staticCacheDir = path.join(this.config.userHome, '.cache/n8n/public'); /** The path to the folder containing custom nodes and credentials */ readonly customExtensionDir = path.join(this.n8nFolder, 'custom'); @@ -58,7 +58,10 @@ export class InstanceSettings { readonly instanceType: InstanceType; - constructor(private readonly config: InstanceSettingsConfig) { + constructor( + private readonly config: InstanceSettingsConfig, + private readonly logger: Logger, + ) { const command = process.argv[2]; this.instanceType = ['webhook', 'worker'].includes(command) ? (command as InstanceType) @@ -154,15 +157,6 @@ export class InstanceSettings { this.save({ ...this.settings, ...newSettings }); } - /** - * The home folder path of the user. - * If none can be found it falls back to the current working directory - */ - private getUserHome() { - const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME'; - return process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd(); - } - /** * Load instance settings from the settings file. If missing, create a new * settings file with an auto-generated encryption key. @@ -198,7 +192,9 @@ export class InstanceSettings { this.save(settings); if (!inTest && !process.env.N8N_ENCRYPTION_KEY) { - console.info(`No encryption key found - Auto-generated and saved to: ${this.settingsFile}`); + this.logger.info( + `No encryption key found - Auto-generated and saved to: ${this.settingsFile}`, + ); } this.ensureSettingsFilePermissions(); @@ -260,11 +256,11 @@ export class InstanceSettings { const permissionsResult = toResult(() => { const stats = statSync(this.settingsFile); - return stats.mode & 0o777; + return stats?.mode & 0o777; }); // If we can't determine the permissions, log a warning and skip the check if (!permissionsResult.ok) { - console.warn( + this.logger.warn( `Could not ensure settings file permissions: ${permissionsResult.error.message}. To skip this check, set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.`, ); return; @@ -277,7 +273,7 @@ export class InstanceSettings { // If the permissions are incorrect and the flag is not set, log a warning if (!this.enforceSettingsFilePermissions.isSet) { - console.warn( + this.logger.warn( `Permissions 0${permissionsResult.result.toString(8)} for n8n settings file ${this.settingsFile} are too wide. This is ignored for now, but in the future n8n will attempt to change the permissions automatically. To automatically enforce correct permissions now set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true (recommended), or turn this check off set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.`, ); // The default is false so we skip the enforcement for now @@ -285,7 +281,7 @@ export class InstanceSettings { } if (this.enforceSettingsFilePermissions.enforce) { - console.warn( + this.logger.warn( `Permissions 0${permissionsResult.result.toString(8)} for n8n settings file ${this.settingsFile} are too wide. Changing permissions to 0600..`, ); const chmodResult = toResult(() => chmodSync(this.settingsFile, 0o600)); @@ -293,7 +289,7 @@ export class InstanceSettings { // Some filesystems don't support permissions. In this case we log the // error and ignore it. We might want to prevent the app startup in the // future in this case. - console.warn( + this.logger.warn( `Could not enforce settings file permissions: ${chmodResult.error.message}. To skip this check, set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.`, ); } diff --git a/packages/core/src/InstanceSettingsConfig.ts b/packages/core/src/InstanceSettingsConfig.ts index 60baf8b80f..dd28472a05 100644 --- a/packages/core/src/InstanceSettingsConfig.ts +++ b/packages/core/src/InstanceSettingsConfig.ts @@ -1,4 +1,5 @@ import { Config, Env } from '@n8n/config'; +import path from 'node:path'; @Config export class InstanceSettingsConfig { @@ -9,4 +10,19 @@ export class InstanceSettingsConfig { */ @Env('N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS') enforceSettingsFilePermissions: boolean = false; + + /** + * The home folder path of the user. + * If none can be found it falls back to the current working directory + */ + readonly userHome: string; + + readonly n8nFolder: string; + + constructor() { + const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME'; + this.userHome = process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd(); + + this.n8nFolder = path.join(this.userHome, '.n8n'); + } } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 20d5ae0833..3a0ebf22f9 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -76,7 +76,6 @@ import type { SchedulingFunctions, } from 'n8n-workflow'; import { - LoggerProxy as Logger, NodeApiError, NodeHelpers, NodeOperationError, @@ -97,6 +96,8 @@ import { Readable } from 'stream'; import Container from 'typedi'; import url, { URL, URLSearchParams } from 'url'; +import { Logger } from '@/logging/logger'; + import { BinaryDataService } from './BinaryData/BinaryData.service'; import type { BinaryData } from './BinaryData/types'; import { binaryToBuffer } from './BinaryData/utils'; @@ -201,7 +202,7 @@ async function generateContentLengthHeader(config: AxiosRequestConfig) { 'content-length': length, }; } catch (error) { - Logger.error('Unable to calculate form data length', { error }); + Container.get(Logger).error('Unable to calculate form data length', { error }); } } @@ -792,7 +793,7 @@ export async function proxyRequestToAxios( error.config = error.request = undefined; error.options = pick(config ?? {}, ['url', 'method', 'data', 'headers']); if (response) { - Logger.debug('Request proxied to Axios failed', { status: response.status }); + Container.get(Logger).debug('Request proxied to Axios failed', { status: response.status }); let responseData = response.data; if (Buffer.isBuffer(responseData) || responseData instanceof Readable) { @@ -1406,7 +1407,7 @@ export async function requestOAuth2( if (isN8nRequest) { return await this.helpers.httpRequest(newRequestOptions).catch(async (error: AxiosError) => { if (error.response?.status === 401) { - Logger.debug( + this.logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`, ); const tokenRefreshOptions: IDataObject = {}; @@ -1425,7 +1426,7 @@ export async function requestOAuth2( let newToken; - Logger.debug( + this.logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`, ); // if it's OAuth2 with client credentials grant type, get a new token @@ -1436,7 +1437,7 @@ export async function requestOAuth2( newToken = await token.refresh(tokenRefreshOptions as unknown as ClientOAuth2Options); } - Logger.debug( + this.logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`, ); @@ -1499,7 +1500,7 @@ export async function requestOAuth2( Authorization: '', }; } - Logger.debug( + this.logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`, ); @@ -1512,7 +1513,7 @@ export async function requestOAuth2( } else { newToken = await token.refresh(tokenRefreshOptions as unknown as ClientOAuth2Options); } - Logger.debug( + this.logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`, ); @@ -1534,7 +1535,7 @@ export async function requestOAuth2( credentials as unknown as ICredentialDataDecryptedObject, ); - Logger.debug( + this.logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`, ); @@ -2562,6 +2563,7 @@ export function getExecuteTriggerFunctions( export function getCredentialTestFunctions(): ICredentialTestFunctions { return { + logger: Container.get(Logger), helpers: { ...getSSHTunnelFunctions(), request: async (uriOrObject: string | object, options?: object) => { diff --git a/packages/core/src/ObjectStore/ObjectStore.service.ee.ts b/packages/core/src/ObjectStore/ObjectStore.service.ee.ts index 0e4d8463df..6379de7789 100644 --- a/packages/core/src/ObjectStore/ObjectStore.service.ee.ts +++ b/packages/core/src/ObjectStore/ObjectStore.service.ee.ts @@ -2,11 +2,13 @@ import { sign } from 'aws4'; import type { Request as Aws4Options, Credentials as Aws4Credentials } from 'aws4'; import axios from 'axios'; import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios'; -import { ApplicationError, LoggerProxy as Logger } from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; import { createHash } from 'node:crypto'; import type { Readable } from 'stream'; import { Service } from 'typedi'; +import { Logger } from '@/logging/logger'; + import type { Bucket, ConfigSchemaCredentials, @@ -30,7 +32,7 @@ export class ObjectStoreService { private isReadOnly = false; - private logger = Logger; + constructor(private readonly logger: Logger) {} async init(host: string, bucket: Bucket, credentials: ConfigSchemaCredentials) { this.host = host; diff --git a/packages/core/src/SerializedBuffer.ts b/packages/core/src/SerializedBuffer.ts index 7a96884729..d6ea874c7a 100644 --- a/packages/core/src/SerializedBuffer.ts +++ b/packages/core/src/SerializedBuffer.ts @@ -1,4 +1,4 @@ -import { isObjectLiteral } from './utils'; +import { isObjectLiteral } from '@/utils'; /** A nodejs Buffer gone through JSON.stringify */ export type SerializedBuffer = { diff --git a/packages/core/src/__tests__/ActiveWorkflows.test.ts b/packages/core/src/__tests__/ActiveWorkflows.test.ts index 85487a0cec..410b4779ba 100644 --- a/packages/core/src/__tests__/ActiveWorkflows.test.ts +++ b/packages/core/src/__tests__/ActiveWorkflows.test.ts @@ -42,7 +42,12 @@ describe('ActiveWorkflows', () => { beforeEach(() => { jest.clearAllMocks(); - activeWorkflows = new ActiveWorkflows(scheduledTaskManager, triggersAndPollers, errorReporter); + activeWorkflows = new ActiveWorkflows( + mock(), + scheduledTaskManager, + triggersAndPollers, + errorReporter, + ); }); type PollTimes = { item: TriggerTime[] }; diff --git a/packages/core/src/error-reporter.ts b/packages/core/src/error-reporter.ts index b6fc936daa..b52fd6d2f6 100644 --- a/packages/core/src/error-reporter.ts +++ b/packages/core/src/error-reporter.ts @@ -2,11 +2,12 @@ import type { NodeOptions } from '@sentry/node'; import { close } from '@sentry/node'; import type { ErrorEvent, EventHint } from '@sentry/types'; import { AxiosError } from 'axios'; -import { ApplicationError, LoggerProxy, type ReportingOptions } from 'n8n-workflow'; +import { ApplicationError, type ReportingOptions } from 'n8n-workflow'; import { createHash } from 'node:crypto'; import { Service } from 'typedi'; import type { InstanceType } from './InstanceSettings'; +import { Logger } from './logging/logger'; @Service() export class ErrorReporter { @@ -15,7 +16,7 @@ export class ErrorReporter { private report: (error: Error | string, options?: ReportingOptions) => void; - constructor() { + constructor(private readonly logger: Logger) { // eslint-disable-next-line @typescript-eslint/unbound-method this.report = this.defaultReport; } @@ -30,7 +31,7 @@ export class ErrorReporter { do { const msg = [e.message + context, e.stack ? `\n${e.stack}\n` : ''].join(''); const meta = e instanceof ApplicationError ? e.extra : undefined; - LoggerProxy.error(msg, meta); + this.logger.error(msg, meta); e = e.cause as Error; } while (e); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bec9767ffa..7abbd9ad9a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,6 +11,7 @@ export * from './Credentials'; export * from './DirectoryLoader'; export * from './Interfaces'; export { InstanceSettings, InstanceType } from './InstanceSettings'; +export { Logger } from './logging/logger'; export * from './NodeExecuteFunctions'; export * from './RoutingNode'; export * from './WorkflowExecute'; diff --git a/packages/cli/src/logging/__tests__/logger.service.test.ts b/packages/core/src/logging/__tests__/logger.test.ts similarity index 93% rename from packages/cli/src/logging/__tests__/logger.service.test.ts rename to packages/core/src/logging/__tests__/logger.test.ts index 2ffbf2120e..d34eaf250a 100644 --- a/packages/cli/src/logging/__tests__/logger.service.test.ts +++ b/packages/core/src/logging/__tests__/logger.test.ts @@ -5,10 +5,11 @@ jest.mock('n8n-workflow', () => ({ import type { GlobalConfig } from '@n8n/config'; import { mock } from 'jest-mock-extended'; -import type { InstanceSettings } from 'n8n-core'; import { LoggerProxy } from 'n8n-workflow'; -import { Logger } from '@/logging/logger.service'; +import type { InstanceSettingsConfig } from '@/InstanceSettingsConfig'; + +import { Logger } from '../logger'; describe('Logger', () => { beforeEach(() => { @@ -25,13 +26,13 @@ describe('Logger', () => { }); test('if root, should initialize `LoggerProxy` with instance', () => { - const logger = new Logger(globalConfig, mock(), { isRoot: true }); + const logger = new Logger(globalConfig, mock(), { isRoot: true }); expect(LoggerProxy.init).toHaveBeenCalledWith(logger); }); test('if scoped, should not initialize `LoggerProxy`', () => { - new Logger(globalConfig, mock(), { isRoot: false }); + new Logger(globalConfig, mock(), { isRoot: false }); expect(LoggerProxy.init).not.toHaveBeenCalled(); }); @@ -47,7 +48,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock()); + const logger = new Logger(globalConfig, mock()); const { transports } = logger.getInternalLogger(); @@ -72,7 +73,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock({ n8nFolder: '/tmp' })); + const logger = new Logger(globalConfig, mock({ n8nFolder: '/tmp' })); const { transports } = logger.getInternalLogger(); @@ -94,7 +95,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock()); + const logger = new Logger(globalConfig, mock()); const internalLogger = logger.getInternalLogger(); @@ -113,7 +114,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock()); + const logger = new Logger(globalConfig, mock()); const internalLogger = logger.getInternalLogger(); @@ -132,7 +133,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock()); + const logger = new Logger(globalConfig, mock()); const internalLogger = logger.getInternalLogger(); @@ -151,7 +152,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock()); + const logger = new Logger(globalConfig, mock()); const internalLogger = logger.getInternalLogger(); @@ -170,7 +171,7 @@ describe('Logger', () => { }, }); - const logger = new Logger(globalConfig, mock()); + const logger = new Logger(globalConfig, mock()); const internalLogger = logger.getInternalLogger(); diff --git a/packages/cli/src/logging/logger.service.ts b/packages/core/src/logging/logger.ts similarity index 89% rename from packages/cli/src/logging/logger.service.ts rename to packages/core/src/logging/logger.ts index 46441e5a33..e959affba3 100644 --- a/packages/cli/src/logging/logger.service.ts +++ b/packages/core/src/logging/logger.ts @@ -2,20 +2,26 @@ import type { LogScope } from '@n8n/config'; import { GlobalConfig } from '@n8n/config'; import callsites from 'callsites'; import type { TransformableInfo } from 'logform'; -import { InstanceSettings, isObjectLiteral } from 'n8n-core'; import { LoggerProxy, LOG_LEVELS } from 'n8n-workflow'; +import type { + Logger as LoggerType, + LogLocationMetadata, + LogLevel, + LogMetadata, +} from 'n8n-workflow'; import path, { basename } from 'node:path'; import pc from 'picocolors'; import { Service } from 'typedi'; import winston from 'winston'; -import { inDevelopment, inProduction } from '@/constants'; +import { inDevelopment, inProduction } from '@/Constants'; +import { InstanceSettingsConfig } from '@/InstanceSettingsConfig'; +import { isObjectLiteral } from '@/utils'; -import { noOp } from './constants'; -import type { LogLocationMetadata, LogLevel, LogMetadata } from './types'; +const noOp = () => {}; @Service() -export class Logger { +export class Logger implements LoggerType { private internalLogger: winston.Logger; private readonly level: LogLevel; @@ -28,7 +34,7 @@ export class Logger { constructor( private readonly globalConfig: GlobalConfig, - private readonly instanceSettings: InstanceSettings, + private readonly instanceSettingsConfig: InstanceSettingsConfig, { isRoot }: { isRoot?: boolean } = { isRoot: true }, ) { this.level = this.globalConfig.logging.level; @@ -49,6 +55,8 @@ export class Logger { if (outputs.includes('file')) this.setFileTransport(); this.scopes = new Set(scopes); + } else { + this.scopes = new Set(); } if (isRoot) LoggerProxy.init(this); @@ -61,7 +69,9 @@ export class Logger { /** Create a logger that injects the given scopes into its log metadata. */ scoped(scopes: LogScope | LogScope[]) { scopes = Array.isArray(scopes) ? scopes : [scopes]; - const scopedLogger = new Logger(this.globalConfig, this.instanceSettings, { isRoot: false }); + const scopedLogger = new Logger(this.globalConfig, this.instanceSettingsConfig, { + isRoot: false, + }); const childLogger = this.internalLogger.child({ scopes }); scopedLogger.setInternalLogger(childLogger); @@ -107,10 +117,10 @@ export class Logger { } private scopeFilter() { - return winston.format((info: TransformableInfo & { metadata: LogMetadata }) => { + return winston.format((info: TransformableInfo) => { if (!this.isScopingEnabled) return info; - const { scopes } = info.metadata; + const { scopes } = (info as unknown as { metadata: LogMetadata }).metadata; const shouldIncludeScope = scopes && scopes?.length > 0 && scopes.some((s) => this.scopes.has(s)); @@ -179,7 +189,7 @@ export class Logger { ); const filename = path.join( - this.instanceSettings.n8nFolder, + this.instanceSettingsConfig.n8nFolder, this.globalConfig.logging.file.location, ); diff --git a/packages/core/src/node-execution-context/node-execution-context.ts b/packages/core/src/node-execution-context/node-execution-context.ts index 8477fe7856..d303b10ba1 100644 --- a/packages/core/src/node-execution-context/node-execution-context.ts +++ b/packages/core/src/node-execution-context/node-execution-context.ts @@ -23,7 +23,6 @@ import { ApplicationError, deepCopy, ExpressionError, - LoggerProxy, NodeHelpers, NodeOperationError, } from 'n8n-workflow'; @@ -33,6 +32,7 @@ import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/Constants import { Memoized } from '@/decorators'; import { extractValue } from '@/ExtractValue'; import { InstanceSettings } from '@/InstanceSettings'; +import { Logger } from '@/logging/logger'; import { cleanupParameterData } from './utils/cleanupParameterData'; import { ensureType } from './utils/ensureType'; @@ -53,8 +53,9 @@ export abstract class NodeExecutionContext implements Omit { const userFolder = '/test'; @@ -11,12 +14,16 @@ describe('InstanceSettings', () => { const settingsFile = `${userFolder}/.n8n/config`; const mockFs = mock(fs); + const logger = mockInstance(Logger); const createInstanceSettings = (opts?: Partial) => - new InstanceSettings({ - ...new InstanceSettingsConfig(), - ...opts, - }); + new InstanceSettings( + { + ...new InstanceSettingsConfig(), + ...opts, + }, + logger, + ); beforeEach(() => { jest.resetAllMocks(); @@ -203,7 +210,7 @@ describe('InstanceSettings', () => { mockFs.readFileSync .calledWith(settingsFile) .mockReturnValue(JSON.stringify({ encryptionKey: 'test_key' })); - settings = new InstanceSettings(mock()); + settings = createInstanceSettings(); }); it('should return true if /.dockerenv exists', () => { diff --git a/packages/core/test/ObjectStore.service.test.ts b/packages/core/test/ObjectStore.service.test.ts index 77936c20f0..9899ad17fc 100644 --- a/packages/core/test/ObjectStore.service.test.ts +++ b/packages/core/test/ObjectStore.service.test.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { mock } from 'jest-mock-extended'; import { Readable } from 'stream'; import { ObjectStoreService } from '@/ObjectStore/ObjectStore.service.ee'; @@ -25,7 +26,7 @@ const toDeletionXml = (filename: string) => ` let objectStoreService: ObjectStoreService; beforeEach(async () => { - objectStoreService = new ObjectStoreService(); + objectStoreService = new ObjectStoreService(mock()); mockAxios.request.mockResolvedValueOnce({ status: 200 }); // for checkConnection await objectStoreService.init(mockHost, mockBucket, mockCredentials); jest.restoreAllMocks(); diff --git a/packages/core/test/error-reporter.test.ts b/packages/core/test/error-reporter.test.ts index 1f507ab5c0..7cd94fdb4b 100644 --- a/packages/core/test/error-reporter.test.ts +++ b/packages/core/test/error-reporter.test.ts @@ -1,6 +1,7 @@ import { QueryFailedError } from '@n8n/typeorm'; import type { ErrorEvent } from '@sentry/types'; import { AxiosError } from 'axios'; +import { mock } from 'jest-mock-extended'; import { ApplicationError } from 'n8n-workflow'; import { ErrorReporter } from '@/error-reporter'; @@ -15,7 +16,7 @@ jest.mock('@sentry/node', () => ({ jest.spyOn(process, 'on'); describe('ErrorReporter', () => { - const errorReporter = new ErrorReporter(); + const errorReporter = new ErrorReporter(mock()); const event = {} as ErrorEvent; describe('beforeSend', () => { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 401bb177c4..654ae1be61 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -15,6 +15,7 @@ "include": ["src/**/*.ts", "test/**/*.ts"], "references": [ { "path": "../workflow/tsconfig.build.json" }, + { "path": "../@n8n/config/tsconfig.build.json" }, { "path": "../@n8n/client-oauth2/tsconfig.build.json" } ] } diff --git a/packages/nodes-base/nodes/Ldap/Helpers.ts b/packages/nodes-base/nodes/Ldap/Helpers.ts index 5e4447fa84..0b56b1b230 100644 --- a/packages/nodes-base/nodes/Ldap/Helpers.ts +++ b/packages/nodes-base/nodes/Ldap/Helpers.ts @@ -1,7 +1,6 @@ import { Client } from 'ldapts'; import type { ClientOptions, Entry } from 'ldapts'; -import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow'; -import { LoggerProxy as Logger } from 'n8n-workflow'; +import type { ICredentialDataDecryptedObject, IDataObject, Logger } from 'n8n-workflow'; export const BINARY_AD_ATTRIBUTES = ['objectGUID', 'objectSid']; const resolveEntryBinaryAttributes = (entry: Entry): Entry => { @@ -18,6 +17,7 @@ export const resolveBinaryAttributes = (entries: Entry[]): void => { }; export async function createLdapClient( + context: { logger: Logger }, credentials: ICredentialDataDecryptedObject, nodeDebug?: boolean, nodeType?: string, @@ -45,7 +45,7 @@ export async function createLdapClient( } if (nodeDebug) { - Logger.info( + context.logger.info( `[${nodeType} | ${nodeName}] - LDAP Options: ${JSON.stringify(ldapOptions, null, 2)}`, ); } diff --git a/packages/nodes-base/nodes/Ldap/Ldap.node.ts b/packages/nodes-base/nodes/Ldap/Ldap.node.ts index 2d13e10314..6c24606a67 100644 --- a/packages/nodes-base/nodes/Ldap/Ldap.node.ts +++ b/packages/nodes-base/nodes/Ldap/Ldap.node.ts @@ -103,7 +103,7 @@ export class Ldap implements INodeType { credential: ICredentialsDecrypted, ): Promise { const credentials = credential.data as ICredentialDataDecryptedObject; - const client = await createLdapClient(credentials); + const client = await createLdapClient(this, credentials); try { await client.bind(credentials.bindDN as string, credentials.bindPassword as string); } catch (error) { @@ -123,7 +123,7 @@ export class Ldap implements INodeType { loadOptions: { async getAttributes(this: ILoadOptionsFunctions) { const credentials = await this.getCredentials('ldap'); - const client = await createLdapClient(credentials); + const client = await createLdapClient(this, credentials); try { await client.bind(credentials.bindDN as string, credentials.bindPassword as string); @@ -153,7 +153,7 @@ export class Ldap implements INodeType { async getObjectClasses(this: ILoadOptionsFunctions) { const credentials = await this.getCredentials('ldap'); - const client = await createLdapClient(credentials); + const client = await createLdapClient(this, credentials); try { await client.bind(credentials.bindDN as string, credentials.bindPassword as string); } catch (error) { @@ -196,7 +196,7 @@ export class Ldap implements INodeType { async getAttributesForDn(this: ILoadOptionsFunctions) { const credentials = await this.getCredentials('ldap'); - const client = await createLdapClient(credentials); + const client = await createLdapClient(this, credentials); try { await client.bind(credentials.bindDN as string, credentials.bindPassword as string); @@ -242,6 +242,7 @@ export class Ldap implements INodeType { const credentials = await this.getCredentials('ldap'); const client = await createLdapClient( + this, credentials, nodeDebug, this.getNode().type, diff --git a/packages/workflow/package.json b/packages/workflow/package.json index cabbeaaaaa..3165343bd0 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -31,6 +31,7 @@ ], "devDependencies": { "@langchain/core": "catalog:", + "@n8n/config": "workspace:*", "@types/deep-equal": "^1.0.1", "@types/express": "catalog:", "@types/jmespath": "^0.15.0", @@ -44,7 +45,7 @@ "@n8n_io/riot-tmpl": "4.0.0", "ast-types": "0.15.2", "axios": "catalog:", - "callsites": "3.1.0", + "callsites": "catalog:", "deep-equal": "2.2.0", "esprima-next": "5.8.4", "form-data": "catalog:", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 6c28d4664d..a67f7b59b4 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ - import type { CallbackManager as CallbackManagerLC } from '@langchain/core/callbacks/manager'; +import type { LogScope } from '@n8n/config'; import type { AxiosProxyConfig, GenericAbortSignal } from 'axios'; import type * as express from 'express'; import type FormData from 'form-data'; @@ -675,6 +675,7 @@ export type ICredentialTestFunction = ( ) => Promise; export interface ICredentialTestFunctions { + logger: Logger; helpers: SSHTunnelFunctions & { request: (uriOrObject: string | object, options?: object) => Promise; }; @@ -2447,7 +2448,17 @@ export interface WorkflowTestData { } export type LogLevel = (typeof LOG_LEVELS)[number]; -export type Logger = Record, (message: string, meta?: object) => void>; +export type LogMetadata = { + [key: string]: unknown; + scopes?: LogScope[]; + file?: string; + function?: string; +}; +export type Logger = Record< + Exclude, + (message: string, metadata?: LogMetadata) => void +>; +export type LogLocationMetadata = Pick; export interface IStatusCodeMessages { [key: string]: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a42e557826..bf87db10a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ catalogs: basic-auth: specifier: 2.0.1 version: 2.0.1 + callsites: + specifier: 3.1.0 + version: 3.1.0 fast-glob: specifier: 3.2.12 version: 3.2.12 @@ -54,6 +57,9 @@ catalogs: nanoid: specifier: 3.3.8 version: 3.3.8 + picocolors: + specifier: 1.0.1 + version: 1.0.1 typedi: specifier: 0.10.0 version: 0.10.0 @@ -811,9 +817,6 @@ importers: cache-manager: specifier: 5.2.3 version: 5.2.3 - callsites: - specifier: 3.1.0 - version: 3.1.0 change-case: specifier: 4.1.2 version: 4.1.2 @@ -947,7 +950,7 @@ importers: specifier: 8.12.0 version: 8.12.0 picocolors: - specifier: 1.0.1 + specifier: 'catalog:' version: 1.0.1 pkce-challenge: specifier: 3.0.0 @@ -1006,9 +1009,6 @@ importers: validator: specifier: 13.7.0 version: 13.7.0 - winston: - specifier: 3.14.2 - version: 3.14.2 ws: specifier: '>=8.17.1' version: 8.17.1 @@ -1136,6 +1136,9 @@ importers: axios: specifier: 'catalog:' version: 1.7.4 + callsites: + specifier: 'catalog:' + version: 3.1.0 chardet: specifier: 2.0.0 version: 2.0.0 @@ -1178,6 +1181,9 @@ importers: p-cancelable: specifier: 2.1.1 version: 2.1.1 + picocolors: + specifier: 'catalog:' + version: 1.0.1 pretty-bytes: specifier: 5.6.0 version: 5.6.0 @@ -1193,6 +1199,9 @@ importers: uuid: specifier: 'catalog:' version: 10.0.0 + winston: + specifier: 3.14.2 + version: 3.14.2 xml2js: specifier: 'catalog:' version: 0.6.2 @@ -1934,7 +1943,7 @@ importers: specifier: 'catalog:' version: 1.7.4 callsites: - specifier: 3.1.0 + specifier: 'catalog:' version: 3.1.0 deep-equal: specifier: 2.2.0 @@ -1979,6 +1988,9 @@ importers: '@langchain/core': specifier: 'catalog:' version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + '@n8n/config': + specifier: workspace:* + version: link:../@n8n/config '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -14448,13 +14460,13 @@ snapshots: '@babel/code-frame@7.24.6': dependencies: '@babel/highlight': 7.24.6 - picocolors: 1.0.1 + picocolors: 1.1.1 '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 js-tokens: 4.0.0 - picocolors: 1.0.1 + picocolors: 1.1.1 '@babel/compat-data@7.23.5': {} @@ -14701,7 +14713,7 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.1 + picocolors: 1.1.1 '@babel/parser@7.25.6': dependencies: @@ -19415,7 +19427,7 @@ snapshots: caniuse-lite: 1.0.30001667 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.1 + picocolors: 1.1.1 postcss: 8.4.38 postcss-value-parser: 4.2.0 @@ -19603,7 +19615,7 @@ snapshots: '@types/readable-stream': 4.0.10 buffer: 6.0.3 inherits: 2.0.4 - readable-stream: 4.4.2 + readable-stream: 4.5.2 blob-util@2.0.2: {} @@ -25032,13 +25044,13 @@ snapshots: postcss@8.4.31: dependencies: nanoid: 3.3.8 - picocolors: 1.0.1 + picocolors: 1.1.1 source-map-js: 1.2.1 postcss@8.4.38: dependencies: nanoid: 3.3.8 - picocolors: 1.0.1 + picocolors: 1.1.1 source-map-js: 1.2.0 postcss@8.4.49: @@ -26496,7 +26508,7 @@ snapshots: css-tree: 2.3.1 css-what: 6.1.0 csso: 5.0.5 - picocolors: 1.0.1 + picocolors: 1.1.1 swagger-ui-dist@5.11.0: {} @@ -26542,7 +26554,7 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.0.1 + picocolors: 1.1.1 postcss: 8.4.38 postcss-import: 15.1.0(postcss@8.4.38) postcss-js: 4.0.1(postcss@8.4.38) @@ -27094,7 +27106,7 @@ snapshots: dependencies: browserslist: 4.23.0 escalade: 3.1.1 - picocolors: 1.0.1 + picocolors: 1.1.1 update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6888cae7ae..9700a3a45b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,7 @@ catalog: '@types/xml2js': ^0.4.14 axios: 1.7.4 basic-auth: 2.0.1 + callsites: 3.1.0 chokidar: 4.0.1 fast-glob: 3.2.12 flatted: 3.2.7 @@ -21,6 +22,7 @@ catalog: lodash: 4.17.21 luxon: 3.4.4 nanoid: 3.3.8 + picocolors: 1.0.1 typedi: 0.10.0 uuid: 10.0.0 xml2js: 0.6.2 From f754b22a3f49c1528887c52902073ce3c0127645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 24 Dec 2024 13:02:05 +0100 Subject: [PATCH 10/66] refactor(core): Mark all backend Enterprise Edition files and dirs (#12350) --- LICENSE.md | 8 +++--- .../{combineScopes.ts => combineScopes.ee.ts} | 2 +- .../src/{constants.ts => constants.ee.ts} | 0 .../src/{hasScope.ts => hasScope.ee.ts} | 4 +-- packages/@n8n/permissions/src/index.ts | 8 +++--- .../permissions/src/{types.ts => types.ee.ts} | 2 +- .../@n8n/permissions/test/hasScope.test.ts | 4 +-- .../workflow-execute-additional-data.test.ts | 4 +-- packages/cli/src/auth/methods/email.ts | 2 +- packages/cli/src/auth/methods/ldap.ts | 4 +-- packages/cli/src/commands/base-command.ts | 4 +-- packages/cli/src/commands/ldap/reset.ts | 2 +- .../__tests__/users.controller.test.ts | 2 +- .../cli/src/controllers/auth.controller.ts | 2 +- .../src/controllers/invitation.controller.ts | 2 +- packages/cli/src/controllers/me.controller.ts | 2 +- .../oauth1-credential.controller.test.ts | 4 +-- .../oauth2-credential.controller.test.ts | 4 +-- .../controllers/password-reset.controller.ts | 2 +- .../cli/src/controllers/project.controller.ts | 2 +- .../cli/src/controllers/users.controller.ts | 2 +- .../src/credentials/credentials.service.ee.ts | 2 +- .../src/credentials/credentials.service.ts | 4 +-- packages/cli/src/databases/entities/user.ts | 2 +- .../1674509946020-CreateLdapEntities.ts | 2 +- .../shared-credentials.repository.test.ts | 2 +- .../repositories/settings.repository.ts | 2 +- .../cli/src/decorators/controller.registry.ts | 2 +- .../source-control-export.service.test.ts | 0 .../source-control-git.service.test.ts | 0 .../source-control-helper.ee.test.ts | 10 +++---- .../__tests__/source-control.service.test.ts | 4 +-- .../source-control/constants.ts | 0 .../source-control-enabled-middleware.ee.ts | 0 .../source-control-export.service.ee.ts | 0 .../source-control-git.service.ee.ts | 0 .../source-control-helper.ee.ts | 0 .../source-control-import.service.ee.ts | 0 .../source-control-preferences.service.ee.ts | 0 .../source-control.controller.ee.ts | 0 .../source-control.service.ee.ts | 0 .../source-control/types/export-result.ts | 0 .../types/exportable-credential.ts | 0 .../types/exportable-workflow.ts | 0 .../source-control/types/import-result.ts | 0 .../source-control/types/key-pair-type.ts | 0 .../source-control/types/key-pair.ts | 0 .../source-control/types/requests.ts | 0 .../source-control/types/resource-owner.ts | 0 .../types/source-control-commit.ts | 0 .../types/source-control-disconnect.ts | 0 .../types/source-control-generate-key-pair.ts | 0 .../types/source-control-get-status.ts | 0 .../types/source-control-preferences.ts | 0 .../types/source-control-pull-work-folder.ts | 0 .../types/source-control-push-work-folder.ts | 0 .../types/source-control-push.ts | 0 .../types/source-control-set-branch.ts | 0 .../types/source-control-set-read-only.ts | 0 .../types/source-control-stage.ts | 0 .../source-control-workflow-version-id.ts | 0 .../types/source-controlled-file.ts | 0 .../variables/environment-helpers.ts | 0 .../variables/variables.controller.ee.ts | 0 .../variables/variables.service.ee.ts | 0 .../metric.schema.ts | 0 .../metrics.controller.ts | 2 +- .../test-definition.schema.ts | 0 .../test-definition.service.ee.ts | 0 .../test-definitions.controller.ee.ts | 4 +-- .../test-definitions.types.ee.ts | 0 .../__tests__/create-pin-data.ee.test.ts | 0 .../__tests__/evaluation-metrics.ee.test.ts | 0 .../__tests__/get-start-node.ee.test.ts | 0 .../__tests__/mock-data/execution-data.json | 0 .../execution-data.multiple-triggers-2.json | 0 .../execution-data.multiple-triggers.json | 0 .../mock-data/workflow.evaluation.json | 0 .../mock-data/workflow.multiple-triggers.json | 0 .../mock-data/workflow.under-test.json | 0 .../__tests__/test-runner.service.ee.test.ts | 0 .../test-runner/evaluation-metrics.ee.ts | 0 .../test-runner/test-runner.service.ee.ts | 0 .../test-runner/utils.ee.ts | 0 .../test-runs.controller.ee.ts | 2 +- ...essage-event-bus-destination-webhook.ee.ts | 2 +- .../external-secrets-manager.ee.test.ts | 4 +-- .../constants.ts | 0 .../external-secrets-helper.ee.ts | 0 .../external-secrets-manager.ee.ts | 0 .../external-secrets-providers.ee.ts | 0 .../external-secrets.controller.ee.ts | 0 .../external-secrets.service.ee.ts | 0 .../__tests__/azure-key-vault.test.ts | 0 .../__tests__/gcp-secrets-manager.test.ts | 0 .../aws-secrets/aws-secrets-client.ts | 0 .../aws-secrets/aws-secrets-manager.ts | 2 +- .../providers/aws-secrets/types.ts | 0 .../azure-key-vault/azure-key-vault.ts | 2 +- .../providers/azure-key-vault/types.ts | 0 .../gcp-secrets-manager.ts | 2 +- .../providers/gcp-secrets-manager/types.ts | 0 .../providers/infisical.ts | 0 .../providers/vault.ts | 0 .../__tests__/helpers.test.ts | 2 +- .../cli/src/{ldap => ldap.ee}/constants.ts | 0 .../cli/src/{ldap => ldap.ee}/helpers.ee.ts | 0 .../{ldap => ldap.ee}/ldap.controller.ee.ts | 0 .../src/{ldap => ldap.ee}/ldap.service.ee.ts | 2 +- packages/cli/src/{ldap => ldap.ee}/types.ts | 0 .../check-access.ts | 0 .../global-roles.ts | 0 .../project-roles.ts | 0 .../resource-roles.ts | 0 .../source-control/source-control.handler.ts | 8 +++--- .../handlers/variables/variables.handler.ts | 2 +- .../handlers/workflows/workflows.handler.ts | 2 +- .../shared/middlewares/global.middleware.ts | 2 +- .../scaling/__tests__/pubsub-handler.test.ts | 4 +-- .../cli/src/scaling/pubsub/pubsub-handler.ts | 4 +-- ...service.ts => worker-status.service.ee.ts} | 0 ...crets-helpers.ts => secrets-helpers.ee.ts} | 2 +- packages/cli/src/server.ts | 26 +++++++++---------- .../__tests__/orchestration.service.test.ts | 2 +- packages/cli/src/services/frontend.service.ts | 10 +++---- ...oject.service.ts => project.service.ee.ts} | 0 packages/cli/src/services/role.service.ts | 6 ++--- .../saml/__tests__/saml-helpers.test.ts | 4 +-- .../saml/__tests__/saml-validator.test.ts | 0 .../saml/__tests__/saml.service.ee.test.ts | 4 +-- .../cli/src/{sso => sso.ee}/saml/constants.ts | 0 .../errors/invalid-saml-metadata.error.ts | 0 .../middleware/saml-enabled-middleware.ts | 0 .../__tests__/saml.controller.ee.test.ts | 0 .../saml/routes/saml.controller.ee.ts | 0 .../src/{sso => sso.ee}/saml/saml-helpers.ts | 0 .../{sso => sso.ee}/saml/saml-validator.ts | 0 .../{sso => sso.ee}/saml/saml.service.ee.ts | 0 .../saml/schema/metadata-exchange.xsd.ts | 0 ...is-200401-wss-wssecurity-secext-1.0.xsd.ts | 0 ...s-200401-wss-wssecurity-utility-1.0.xsd.ts | 0 .../schema/saml-schema-assertion-2.0.xsd.ts | 0 .../schema/saml-schema-metadata-2.0.xsd.ts | 0 .../schema/saml-schema-protocol-2.0.xsd.ts | 0 .../saml/schema/ws-addr.xsd.ts | 0 .../saml/schema/ws-authorization.xsd.ts | 0 .../saml/schema/ws-federation.xsd.ts | 0 .../saml/schema/ws-securitypolicy-1.2.xsd.ts | 0 .../saml/schema/xenc-schema.xsd.ts | 0 .../{sso => sso.ee}/saml/schema/xml.xsd.ts | 0 .../saml/schema/xmldsig-core-schema.xsd.ts | 0 .../saml/service-provider.ee.ts | 0 .../src/{sso => sso.ee}/saml/types/index.ts | 0 .../{sso => sso.ee}/saml/types/requests.ts | 0 .../saml/types/saml-attribute-mapping.ts | 0 .../saml/types/saml-preferences.ts | 0 .../saml/types/saml-user-attributes.ts | 0 .../saml/views/init-sso-post.ts | 0 .../cli/src/{sso => sso.ee}/sso-helpers.ts | 0 packages/cli/src/telemetry/index.ts | 2 +- .../src/user-management/permission-checker.ts | 2 +- .../src/workflow-execute-additional-data.ts | 2 +- packages/cli/src/workflow-helpers.ts | 2 +- .../workflow-history-helper.ee.test.ts | 2 +- .../workflow-history.service.ee.test.ts | 4 +-- .../workflow-history-helper.ee.ts | 0 .../workflow-history-manager.ee.ts | 0 .../workflow-history.controller.ee.ts | 0 .../workflow-history.service.ee.ts | 0 .../cli/src/workflows/workflow.service.ee.ts | 2 +- .../cli/src/workflows/workflow.service.ts | 4 +-- .../cli/src/workflows/workflows.controller.ts | 4 +-- .../active-workflow-manager.test.ts | 2 +- .../integration/commands/ldap/reset.test.ts | 4 +-- .../integration/commands/worker.cmd.test.ts | 2 +- .../credentials/credentials.api.ee.test.ts | 2 +- .../source-control-import.service.test.ts | 6 ++--- .../environments/source-control.test.ts | 6 ++--- .../evaluation/test-definitions.api.test.ts | 2 +- .../external-secrets.api.test.ts | 4 +-- .../test/integration/ldap/ldap.api.test.ts | 11 +++++--- .../integration/password-reset.api.test.ts | 2 +- .../project.service.integration.test.ts | 2 +- .../integration/public-api/workflows.test.ts | 2 +- .../integration/saml/saml-helpers.test.ts | 4 +-- .../test/integration/saml/saml.api.test.ts | 7 +++-- .../services/project.service.test.ts | 2 +- packages/cli/test/integration/shared/ldap.ts | 4 +-- .../integration/shared/utils/test-server.ts | 22 ++++++++-------- .../cli/test/integration/variables.test.ts | 2 +- .../workflow-history-manager.test.ts | 2 +- .../workflow-sharing.service.test.ts | 2 +- .../workflows/workflows.controller.test.ts | 2 +- packages/cli/tsconfig.json | 2 +- 194 files changed, 161 insertions(+), 153 deletions(-) rename packages/@n8n/permissions/src/{combineScopes.ts => combineScopes.ee.ts} (98%) rename packages/@n8n/permissions/src/{constants.ts => constants.ee.ts} (100%) rename packages/@n8n/permissions/src/{hasScope.ts => hasScope.ee.ts} (90%) rename packages/@n8n/permissions/src/{types.ts => types.ee.ts} (96%) rename packages/cli/src/{environments => environments.ee}/source-control/__tests__/source-control-export.service.test.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/__tests__/source-control-git.service.test.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/__tests__/source-control-helper.ee.test.ts (93%) rename packages/cli/src/{environments => environments.ee}/source-control/__tests__/source-control.service.test.ts (82%) rename packages/cli/src/{environments => environments.ee}/source-control/constants.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/middleware/source-control-enabled-middleware.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control-export.service.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control-git.service.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control-helper.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control-import.service.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control-preferences.service.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control.controller.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/source-control.service.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/export-result.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/exportable-credential.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/exportable-workflow.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/import-result.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/key-pair-type.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/key-pair.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/requests.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/resource-owner.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-commit.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-disconnect.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-generate-key-pair.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-get-status.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-preferences.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-pull-work-folder.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-push-work-folder.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-push.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-set-branch.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-set-read-only.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-stage.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-control-workflow-version-id.ts (100%) rename packages/cli/src/{environments => environments.ee}/source-control/types/source-controlled-file.ts (100%) rename packages/cli/src/{environments => environments.ee}/variables/environment-helpers.ts (100%) rename packages/cli/src/{environments => environments.ee}/variables/variables.controller.ee.ts (100%) rename packages/cli/src/{environments => environments.ee}/variables/variables.service.ee.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/metric.schema.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/metrics.controller.ts (99%) rename packages/cli/src/{evaluation => evaluation.ee}/test-definition.schema.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-definition.service.ee.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-definitions.controller.ee.ts (97%) rename packages/cli/src/{evaluation => evaluation.ee}/test-definitions.types.ee.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/create-pin-data.ee.test.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/evaluation-metrics.ee.test.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/get-start-node.ee.test.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/mock-data/execution-data.json (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/mock-data/workflow.evaluation.json (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/mock-data/workflow.multiple-triggers.json (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/mock-data/workflow.under-test.json (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/__tests__/test-runner.service.ee.test.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/evaluation-metrics.ee.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/test-runner.service.ee.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runner/utils.ee.ts (100%) rename packages/cli/src/{evaluation => evaluation.ee}/test-runs.controller.ee.ts (96%) rename packages/cli/src/{external-secrets => external-secrets.ee}/__tests__/external-secrets-manager.ee.test.ts (95%) rename packages/cli/src/{external-secrets => external-secrets.ee}/constants.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/external-secrets-helper.ee.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/external-secrets-manager.ee.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/external-secrets-providers.ee.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/external-secrets.controller.ee.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/external-secrets.service.ee.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/__tests__/azure-key-vault.test.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/__tests__/gcp-secrets-manager.test.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/aws-secrets/aws-secrets-client.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/aws-secrets/aws-secrets-manager.ts (99%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/aws-secrets/types.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/azure-key-vault/azure-key-vault.ts (99%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/azure-key-vault/types.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/gcp-secrets-manager/gcp-secrets-manager.ts (99%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/gcp-secrets-manager/types.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/infisical.ts (100%) rename packages/cli/src/{external-secrets => external-secrets.ee}/providers/vault.ts (100%) rename packages/cli/src/{ldap => ldap.ee}/__tests__/helpers.test.ts (96%) rename packages/cli/src/{ldap => ldap.ee}/constants.ts (100%) rename packages/cli/src/{ldap => ldap.ee}/helpers.ee.ts (100%) rename packages/cli/src/{ldap => ldap.ee}/ldap.controller.ee.ts (100%) rename packages/cli/src/{ldap => ldap.ee}/ldap.service.ee.ts (99%) rename packages/cli/src/{ldap => ldap.ee}/types.ts (100%) rename packages/cli/src/{permissions => permissions.ee}/check-access.ts (100%) rename packages/cli/src/{permissions => permissions.ee}/global-roles.ts (100%) rename packages/cli/src/{permissions => permissions.ee}/project-roles.ts (100%) rename packages/cli/src/{permissions => permissions.ee}/resource-roles.ts (100%) rename packages/cli/src/scaling/{worker-status.service.ts => worker-status.service.ee.ts} (100%) rename packages/cli/src/{secrets-helpers.ts => secrets-helpers.ee.ts} (90%) rename packages/cli/src/services/{project.service.ts => project.service.ee.ts} (100%) rename packages/cli/src/{sso => sso.ee}/saml/__tests__/saml-helpers.test.ts (92%) rename packages/cli/src/{sso => sso.ee}/saml/__tests__/saml-validator.test.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/__tests__/saml.service.ee.test.ts (97%) rename packages/cli/src/{sso => sso.ee}/saml/constants.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/errors/invalid-saml-metadata.error.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/middleware/saml-enabled-middleware.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/routes/__tests__/saml.controller.ee.test.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/routes/saml.controller.ee.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/saml-helpers.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/saml-validator.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/saml.service.ee.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/metadata-exchange.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/saml-schema-assertion-2.0.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/saml-schema-metadata-2.0.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/saml-schema-protocol-2.0.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/ws-addr.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/ws-authorization.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/ws-federation.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/ws-securitypolicy-1.2.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/xenc-schema.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/xml.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/schema/xmldsig-core-schema.xsd.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/service-provider.ee.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/types/index.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/types/requests.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/types/saml-attribute-mapping.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/types/saml-preferences.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/types/saml-user-attributes.ts (100%) rename packages/cli/src/{sso => sso.ee}/saml/views/init-sso-post.ts (100%) rename packages/cli/src/{sso => sso.ee}/sso-helpers.ts (100%) rename packages/cli/src/workflows/{workflow-history => workflow-history.ee}/__tests__/workflow-history-helper.ee.test.ts (97%) rename packages/cli/src/workflows/{workflow-history => workflow-history.ee}/__tests__/workflow-history.service.ee.test.ts (96%) rename packages/cli/src/workflows/{workflow-history => workflow-history.ee}/workflow-history-helper.ee.ts (100%) rename packages/cli/src/workflows/{workflow-history => workflow-history.ee}/workflow-history-manager.ee.ts (100%) rename packages/cli/src/workflows/{workflow-history => workflow-history.ee}/workflow-history.controller.ee.ts (100%) rename packages/cli/src/workflows/{workflow-history => workflow-history.ee}/workflow-history.service.ee.ts (100%) diff --git a/LICENSE.md b/LICENSE.md index aab68b6d93..f85f59baa9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,9 +3,11 @@ Portions of this software are licensed as follows: - Content of branches other than the main branch (i.e. "master") are not licensed. -- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License. - To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License - specifically allowing you access to such source code files and as defined in "LICENSE_EE.md". +- Source code files that contain ".ee." in their filename or ".ee" in their dirname are NOT licensed under + the Sustainable Use License. + To use source code files that contain ".ee." in their filename or ".ee" in their dirname you must hold a + valid n8n Enterprise License specifically allowing you access to such source code files and as defined + in "LICENSE_EE.md". - All third party components incorporated into the n8n Software are licensed under the original license provided by the owner of the applicable component. - Content outside of the above mentioned files or restrictions is available under the "Sustainable Use diff --git a/packages/@n8n/permissions/src/combineScopes.ts b/packages/@n8n/permissions/src/combineScopes.ee.ts similarity index 98% rename from packages/@n8n/permissions/src/combineScopes.ts rename to packages/@n8n/permissions/src/combineScopes.ee.ts index 23da64d837..96a43b940c 100644 --- a/packages/@n8n/permissions/src/combineScopes.ts +++ b/packages/@n8n/permissions/src/combineScopes.ee.ts @@ -1,4 +1,4 @@ -import type { Scope, ScopeLevels, GlobalScopes, MaskLevels } from './types'; +import type { Scope, ScopeLevels, GlobalScopes, MaskLevels } from './types.ee'; export function combineScopes(userScopes: GlobalScopes, masks?: MaskLevels): Set; export function combineScopes(userScopes: ScopeLevels, masks?: MaskLevels): Set; diff --git a/packages/@n8n/permissions/src/constants.ts b/packages/@n8n/permissions/src/constants.ee.ts similarity index 100% rename from packages/@n8n/permissions/src/constants.ts rename to packages/@n8n/permissions/src/constants.ee.ts diff --git a/packages/@n8n/permissions/src/hasScope.ts b/packages/@n8n/permissions/src/hasScope.ee.ts similarity index 90% rename from packages/@n8n/permissions/src/hasScope.ts rename to packages/@n8n/permissions/src/hasScope.ee.ts index d449283490..81bcbc5175 100644 --- a/packages/@n8n/permissions/src/hasScope.ts +++ b/packages/@n8n/permissions/src/hasScope.ee.ts @@ -1,5 +1,5 @@ -import { combineScopes } from './combineScopes'; -import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions, MaskLevels } from './types'; +import { combineScopes } from './combineScopes.ee'; +import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions, MaskLevels } from './types.ee'; export function hasScope( scope: Scope | Scope[], diff --git a/packages/@n8n/permissions/src/index.ts b/packages/@n8n/permissions/src/index.ts index f04f2e4ef6..ae20358303 100644 --- a/packages/@n8n/permissions/src/index.ts +++ b/packages/@n8n/permissions/src/index.ts @@ -1,4 +1,4 @@ -export type * from './types'; -export * from './constants'; -export * from './hasScope'; -export * from './combineScopes'; +export type * from './types.ee'; +export * from './constants.ee'; +export * from './hasScope.ee'; +export * from './combineScopes.ee'; diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ee.ts similarity index 96% rename from packages/@n8n/permissions/src/types.ts rename to packages/@n8n/permissions/src/types.ee.ts index b36fb792ae..db74668fbe 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ee.ts @@ -1,4 +1,4 @@ -import type { RESOURCES } from './constants'; +import type { RESOURCES } from './constants.ee'; export type Resource = keyof typeof RESOURCES; diff --git a/packages/@n8n/permissions/test/hasScope.test.ts b/packages/@n8n/permissions/test/hasScope.test.ts index 0e43bc8dc6..b3362e4ea6 100644 --- a/packages/@n8n/permissions/test/hasScope.test.ts +++ b/packages/@n8n/permissions/test/hasScope.test.ts @@ -1,5 +1,5 @@ -import { hasScope } from '@/hasScope'; -import type { Scope } from '@/types'; +import { hasScope } from '@/hasScope.ee'; +import type { Scope } from '@/types.ee'; const ownerPermissions: Scope[] = [ 'workflow:create', diff --git a/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts index e7d94d3e34..641e239393 100644 --- a/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts +++ b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts @@ -15,10 +15,10 @@ import { CredentialsHelper } from '@/credentials-helper'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { VariablesService } from '@/environments/variables/variables.service.ee'; +import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; -import { SecretsHelper } from '@/secrets-helpers'; +import { SecretsHelper } from '@/secrets-helpers.ee'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service'; import { Telemetry } from '@/telemetry'; diff --git a/packages/cli/src/auth/methods/email.ts b/packages/cli/src/auth/methods/email.ts index 723365057f..6f378e4357 100644 --- a/packages/cli/src/auth/methods/email.ts +++ b/packages/cli/src/auth/methods/email.ts @@ -4,7 +4,7 @@ import type { User } from '@/databases/entities/user'; import { UserRepository } from '@/databases/repositories/user.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { EventService } from '@/events/event.service'; -import { isLdapLoginEnabled } from '@/ldap/helpers.ee'; +import { isLdapLoginEnabled } from '@/ldap.ee/helpers.ee'; import { PasswordUtility } from '@/services/password.utility'; export const handleEmailLogin = async ( diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts index b8fea25989..f067994215 100644 --- a/packages/cli/src/auth/methods/ldap.ts +++ b/packages/cli/src/auth/methods/ldap.ts @@ -10,8 +10,8 @@ import { mapLdapAttributesToUser, createLdapAuthIdentity, updateLdapUserOnLocalDb, -} from '@/ldap/helpers.ee'; -import { LdapService } from '@/ldap/ldap.service.ee'; +} from '@/ldap.ee/helpers.ee'; +import { LdapService } from '@/ldap.ee/ldap.service.ee'; export const handleLdapLogin = async ( loginId: string, diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index a10c386f42..7692ef79be 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -23,13 +23,13 @@ import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus' import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; import { initExpressionEvaluator } from '@/expression-evaluator'; import { ExternalHooks } from '@/external-hooks'; -import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; +import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { NodeTypes } from '@/node-types'; import { PostHogClient } from '@/posthog'; import { ShutdownService } from '@/shutdown/shutdown.service'; -import { WorkflowHistoryManager } from '@/workflows/workflow-history/workflow-history-manager.ee'; +import { WorkflowHistoryManager } from '@/workflows/workflow-history.ee/workflow-history-manager.ee'; export abstract class BaseCommand extends Command { protected logger = Container.get(Logger); diff --git a/packages/cli/src/commands/ldap/reset.ts b/packages/cli/src/commands/ldap/reset.ts index f9f1f3d0bb..6aaaa217d6 100644 --- a/packages/cli/src/commands/ldap/reset.ts +++ b/packages/cli/src/commands/ldap/reset.ts @@ -14,7 +14,7 @@ import { SettingsRepository } from '@/databases/repositories/settings.repository import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap/constants'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; import { WorkflowService } from '@/workflows/workflow.service'; import { BaseCommand } from '../base-command'; diff --git a/packages/cli/src/controllers/__tests__/users.controller.test.ts b/packages/cli/src/controllers/__tests__/users.controller.test.ts index 91ad6bd28b..5eaaca5840 100644 --- a/packages/cli/src/controllers/__tests__/users.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/users.controller.test.ts @@ -4,7 +4,7 @@ import type { User } from '@/databases/entities/user'; import type { UserRepository } from '@/databases/repositories/user.repository'; import type { EventService } from '@/events/event.service'; import type { AuthenticatedRequest } from '@/requests'; -import type { ProjectService } from '@/services/project.service'; +import type { ProjectService } from '@/services/project.service.ee'; import { UsersController } from '../users.controller'; diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index faf24ed669..b012e57131 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -23,7 +23,7 @@ import { getCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, -} from '@/sso/sso-helpers'; +} from '@/sso.ee/sso-helpers'; @RestController() export class AuthController { diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 24cff10c64..d28b0671e0 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -17,7 +17,7 @@ import { PostHogClient } from '@/posthog'; import { UserRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; import { UserService } from '@/services/user.service'; -import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers'; +import { isSamlLicensedAndEnabled } from '@/sso.ee/saml/saml-helpers'; @RestController('/invitations') export class InvitationController { diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index f56d62aebd..5de2e49e96 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -21,7 +21,7 @@ import { MfaService } from '@/mfa/mfa.service'; import { AuthenticatedRequest, MeRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; import { UserService } from '@/services/user.service'; -import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers'; +import { isSamlLicensedAndEnabled } from '@/sso.ee/saml/saml-helpers'; import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto'; @RestController('/me') diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts index c7990ddbfd..0c51f29f22 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts @@ -13,12 +13,12 @@ import type { CredentialsEntity } from '@/databases/entities/credentials-entity' import type { User } from '@/databases/entities/user'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; -import { VariablesService } from '@/environments/variables/variables.service.ee'; +import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import type { OAuthRequest } from '@/requests'; -import { SecretsHelper } from '@/secrets-helpers'; +import { SecretsHelper } from '@/secrets-helpers.ee'; import { mockInstance } from '@test/mocking'; describe('OAuth1CredentialController', () => { diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts index ca9e4db5c6..c1c240425e 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts @@ -13,12 +13,12 @@ import type { CredentialsEntity } from '@/databases/entities/credentials-entity' import type { User } from '@/databases/entities/user'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; -import { VariablesService } from '@/environments/variables/variables.service.ee'; +import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import type { OAuthRequest } from '@/requests'; -import { SecretsHelper } from '@/secrets-helpers'; +import { SecretsHelper } from '@/secrets-helpers.ee'; import { mockInstance } from '@test/mocking'; describe('OAuth2CredentialController', () => { diff --git a/packages/cli/src/controllers/password-reset.controller.ts b/packages/cli/src/controllers/password-reset.controller.ts index cb5e2c6f8b..7fc266c8e8 100644 --- a/packages/cli/src/controllers/password-reset.controller.ts +++ b/packages/cli/src/controllers/password-reset.controller.ts @@ -18,7 +18,7 @@ import { MfaService } from '@/mfa/mfa.service'; import { PasswordResetRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; import { UserService } from '@/services/user.service'; -import { isSamlCurrentAuthenticationMethod } from '@/sso/sso-helpers'; +import { isSamlCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers'; import { UserManagementMailer } from '@/user-management/email'; @RestController() diff --git a/packages/cli/src/controllers/project.controller.ts b/packages/cli/src/controllers/project.controller.ts index 415e0e9519..d16883f3dd 100644 --- a/packages/cli/src/controllers/project.controller.ts +++ b/packages/cli/src/controllers/project.controller.ts @@ -23,7 +23,7 @@ import { ProjectService, TeamProjectOverQuotaError, UnlicensedProjectRoleError, -} from '@/services/project.service'; +} from '@/services/project.service.ee'; import { RoleService } from '@/services/role.service'; @RestController('/projects') diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 4cfa18a1e3..e290d29463 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -21,7 +21,7 @@ import { ExternalHooks } from '@/external-hooks'; import type { PublicUser } from '@/interfaces'; import { listQueryMiddleware } from '@/middlewares'; import { AuthenticatedRequest, ListQuery, UserRequest } from '@/requests'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { UserService } from '@/services/user.service'; import { WorkflowService } from '@/workflows/workflow.service'; diff --git a/packages/cli/src/credentials/credentials.service.ee.ts b/packages/cli/src/credentials/credentials.service.ee.ts index aad78fe7b7..274feff81b 100644 --- a/packages/cli/src/credentials/credentials.service.ee.ts +++ b/packages/cli/src/credentials/credentials.service.ee.ts @@ -11,7 +11,7 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { TransferCredentialError } from '@/errors/response-errors/transfer-credential.error'; import { OwnershipService } from '@/services/ownership.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { RoleService } from '@/services/role.service'; import { CredentialsService } from './credentials.service'; diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 8261b13649..3525aecbe9 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -33,11 +33,11 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import type { ICredentialsDb } from '@/interfaces'; -import { userHasScopes } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions.ee/check-access'; import type { CredentialRequest, ListQuery } from '@/requests'; import { CredentialsTester } from '@/services/credentials-tester.service'; import { OwnershipService } from '@/services/ownership.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { RoleService } from '@/services/role.service'; export type CredentialsGetSharedOptions = diff --git a/packages/cli/src/databases/entities/user.ts b/packages/cli/src/databases/entities/user.ts index b75bec757c..5aae21d003 100644 --- a/packages/cli/src/databases/entities/user.ts +++ b/packages/cli/src/databases/entities/user.ts @@ -18,7 +18,7 @@ import { GLOBAL_OWNER_SCOPES, GLOBAL_MEMBER_SCOPES, GLOBAL_ADMIN_SCOPES, -} from '@/permissions/global-roles'; +} from '@/permissions.ee/global-roles'; import { NoUrl } from '@/validators/no-url.validator'; import { NoXss } from '@/validators/no-xss.validator'; diff --git a/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts b/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts index 5ccd0c40a4..95a2c11a51 100644 --- a/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts +++ b/packages/cli/src/databases/migrations/common/1674509946020-CreateLdapEntities.ts @@ -1,5 +1,5 @@ import type { MigrationContext, ReversibleMigration } from '@/databases/types'; -import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap/constants'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; export class CreateLdapEntities1674509946020 implements ReversibleMigration { async up({ escape, dbType, isMysql, runQuery }: MigrationContext) { diff --git a/packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts b/packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts index afaca2206f..5f8b441235 100644 --- a/packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts +++ b/packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts @@ -7,7 +7,7 @@ import type { CredentialsEntity } from '@/databases/entities/credentials-entity' import { SharedCredentials } from '@/databases/entities/shared-credentials'; import type { User } from '@/databases/entities/user'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; -import { GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES } from '@/permissions/global-roles'; +import { GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES } from '@/permissions.ee/global-roles'; import { mockEntityManager } from '@test/mocking'; describe('SharedCredentialsRepository', () => { diff --git a/packages/cli/src/databases/repositories/settings.repository.ts b/packages/cli/src/databases/repositories/settings.repository.ts index aa4410d6d4..a00f52b4e7 100644 --- a/packages/cli/src/databases/repositories/settings.repository.ts +++ b/packages/cli/src/databases/repositories/settings.repository.ts @@ -3,7 +3,7 @@ import { ErrorReporter } from 'n8n-core'; import { Service } from 'typedi'; import config from '@/config'; -import { EXTERNAL_SECRETS_DB_KEY } from '@/external-secrets/constants'; +import { EXTERNAL_SECRETS_DB_KEY } from '@/external-secrets.ee/constants'; import { Settings } from '../entities/settings'; diff --git a/packages/cli/src/decorators/controller.registry.ts b/packages/cli/src/decorators/controller.registry.ts index 3a22090db1..fd0cbf4eec 100644 --- a/packages/cli/src/decorators/controller.registry.ts +++ b/packages/cli/src/decorators/controller.registry.ts @@ -11,7 +11,7 @@ import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error'; import type { BooleanLicenseFeature } from '@/interfaces'; import { License } from '@/license'; -import { userHasScopes } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions.ee/check-access'; import type { AuthenticatedRequest } from '@/requests'; import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file diff --git a/packages/cli/src/environments/source-control/__tests__/source-control-export.service.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts similarity index 100% rename from packages/cli/src/environments/source-control/__tests__/source-control-export.service.test.ts rename to packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts diff --git a/packages/cli/src/environments/source-control/__tests__/source-control-git.service.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-git.service.test.ts similarity index 100% rename from packages/cli/src/environments/source-control/__tests__/source-control-git.service.test.ts rename to packages/cli/src/environments.ee/source-control/__tests__/source-control-git.service.test.ts diff --git a/packages/cli/src/environments/source-control/__tests__/source-control-helper.ee.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts similarity index 93% rename from packages/cli/src/environments/source-control/__tests__/source-control-helper.ee.test.ts rename to packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts index 508d1eb49a..5ecb04cf8a 100644 --- a/packages/cli/src/environments/source-control/__tests__/source-control-helper.ee.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts @@ -6,7 +6,7 @@ import Container from 'typedi'; import { SOURCE_CONTROL_SSH_FOLDER, SOURCE_CONTROL_GIT_FOLDER, -} from '@/environments/source-control/constants'; +} from '@/environments.ee/source-control/constants'; import { generateSshKeyPair, getRepoType, @@ -14,10 +14,10 @@ import { getTrackingInformationFromPrePushResult, getTrackingInformationFromPullResult, sourceControlFoldersExistCheck, -} from '@/environments/source-control/source-control-helper.ee'; -import { SourceControlPreferencesService } from '@/environments/source-control/source-control-preferences.service.ee'; -import type { SourceControlPreferences } from '@/environments/source-control/types/source-control-preferences'; -import type { SourceControlledFile } from '@/environments/source-control/types/source-controlled-file'; +} from '@/environments.ee/source-control/source-control-helper.ee'; +import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; +import type { SourceControlPreferences } from '@/environments.ee/source-control/types/source-control-preferences'; +import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file'; import { License } from '@/license'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts similarity index 82% rename from packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts rename to packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts index 1599f7b4bd..8b32062d55 100644 --- a/packages/cli/src/environments/source-control/__tests__/source-control.service.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts @@ -2,8 +2,8 @@ import { mock } from 'jest-mock-extended'; import { InstanceSettings } from 'n8n-core'; import { Container } from 'typedi'; -import { SourceControlPreferencesService } from '@/environments/source-control/source-control-preferences.service.ee'; -import { SourceControlService } from '@/environments/source-control/source-control.service.ee'; +import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; +import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee'; describe('SourceControlService', () => { const preferencesService = new SourceControlPreferencesService( diff --git a/packages/cli/src/environments/source-control/constants.ts b/packages/cli/src/environments.ee/source-control/constants.ts similarity index 100% rename from packages/cli/src/environments/source-control/constants.ts rename to packages/cli/src/environments.ee/source-control/constants.ts diff --git a/packages/cli/src/environments/source-control/middleware/source-control-enabled-middleware.ee.ts b/packages/cli/src/environments.ee/source-control/middleware/source-control-enabled-middleware.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/middleware/source-control-enabled-middleware.ee.ts rename to packages/cli/src/environments.ee/source-control/middleware/source-control-enabled-middleware.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control-export.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-export.service.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control-export.service.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control-export.service.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control-git.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-git.service.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control-git.service.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control-git.service.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control-helper.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-helper.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control-helper.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control-helper.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control-import.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-import.service.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control-import.service.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control-import.service.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control-preferences.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-preferences.service.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control-preferences.service.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control-preferences.service.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control.controller.ee.ts b/packages/cli/src/environments.ee/source-control/source-control.controller.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control.controller.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control.controller.ee.ts diff --git a/packages/cli/src/environments/source-control/source-control.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control.service.ee.ts similarity index 100% rename from packages/cli/src/environments/source-control/source-control.service.ee.ts rename to packages/cli/src/environments.ee/source-control/source-control.service.ee.ts diff --git a/packages/cli/src/environments/source-control/types/export-result.ts b/packages/cli/src/environments.ee/source-control/types/export-result.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/export-result.ts rename to packages/cli/src/environments.ee/source-control/types/export-result.ts diff --git a/packages/cli/src/environments/source-control/types/exportable-credential.ts b/packages/cli/src/environments.ee/source-control/types/exportable-credential.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/exportable-credential.ts rename to packages/cli/src/environments.ee/source-control/types/exportable-credential.ts diff --git a/packages/cli/src/environments/source-control/types/exportable-workflow.ts b/packages/cli/src/environments.ee/source-control/types/exportable-workflow.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/exportable-workflow.ts rename to packages/cli/src/environments.ee/source-control/types/exportable-workflow.ts diff --git a/packages/cli/src/environments/source-control/types/import-result.ts b/packages/cli/src/environments.ee/source-control/types/import-result.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/import-result.ts rename to packages/cli/src/environments.ee/source-control/types/import-result.ts diff --git a/packages/cli/src/environments/source-control/types/key-pair-type.ts b/packages/cli/src/environments.ee/source-control/types/key-pair-type.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/key-pair-type.ts rename to packages/cli/src/environments.ee/source-control/types/key-pair-type.ts diff --git a/packages/cli/src/environments/source-control/types/key-pair.ts b/packages/cli/src/environments.ee/source-control/types/key-pair.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/key-pair.ts rename to packages/cli/src/environments.ee/source-control/types/key-pair.ts diff --git a/packages/cli/src/environments/source-control/types/requests.ts b/packages/cli/src/environments.ee/source-control/types/requests.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/requests.ts rename to packages/cli/src/environments.ee/source-control/types/requests.ts diff --git a/packages/cli/src/environments/source-control/types/resource-owner.ts b/packages/cli/src/environments.ee/source-control/types/resource-owner.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/resource-owner.ts rename to packages/cli/src/environments.ee/source-control/types/resource-owner.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-commit.ts b/packages/cli/src/environments.ee/source-control/types/source-control-commit.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-commit.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-commit.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-disconnect.ts b/packages/cli/src/environments.ee/source-control/types/source-control-disconnect.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-disconnect.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-disconnect.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-generate-key-pair.ts b/packages/cli/src/environments.ee/source-control/types/source-control-generate-key-pair.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-generate-key-pair.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-generate-key-pair.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-get-status.ts b/packages/cli/src/environments.ee/source-control/types/source-control-get-status.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-get-status.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-get-status.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-preferences.ts b/packages/cli/src/environments.ee/source-control/types/source-control-preferences.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-preferences.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-preferences.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-pull-work-folder.ts b/packages/cli/src/environments.ee/source-control/types/source-control-pull-work-folder.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-pull-work-folder.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-pull-work-folder.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-push-work-folder.ts b/packages/cli/src/environments.ee/source-control/types/source-control-push-work-folder.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-push-work-folder.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-push-work-folder.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-push.ts b/packages/cli/src/environments.ee/source-control/types/source-control-push.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-push.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-push.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-set-branch.ts b/packages/cli/src/environments.ee/source-control/types/source-control-set-branch.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-set-branch.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-set-branch.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-set-read-only.ts b/packages/cli/src/environments.ee/source-control/types/source-control-set-read-only.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-set-read-only.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-set-read-only.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-stage.ts b/packages/cli/src/environments.ee/source-control/types/source-control-stage.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-stage.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-stage.ts diff --git a/packages/cli/src/environments/source-control/types/source-control-workflow-version-id.ts b/packages/cli/src/environments.ee/source-control/types/source-control-workflow-version-id.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-control-workflow-version-id.ts rename to packages/cli/src/environments.ee/source-control/types/source-control-workflow-version-id.ts diff --git a/packages/cli/src/environments/source-control/types/source-controlled-file.ts b/packages/cli/src/environments.ee/source-control/types/source-controlled-file.ts similarity index 100% rename from packages/cli/src/environments/source-control/types/source-controlled-file.ts rename to packages/cli/src/environments.ee/source-control/types/source-controlled-file.ts diff --git a/packages/cli/src/environments/variables/environment-helpers.ts b/packages/cli/src/environments.ee/variables/environment-helpers.ts similarity index 100% rename from packages/cli/src/environments/variables/environment-helpers.ts rename to packages/cli/src/environments.ee/variables/environment-helpers.ts diff --git a/packages/cli/src/environments/variables/variables.controller.ee.ts b/packages/cli/src/environments.ee/variables/variables.controller.ee.ts similarity index 100% rename from packages/cli/src/environments/variables/variables.controller.ee.ts rename to packages/cli/src/environments.ee/variables/variables.controller.ee.ts diff --git a/packages/cli/src/environments/variables/variables.service.ee.ts b/packages/cli/src/environments.ee/variables/variables.service.ee.ts similarity index 100% rename from packages/cli/src/environments/variables/variables.service.ee.ts rename to packages/cli/src/environments.ee/variables/variables.service.ee.ts diff --git a/packages/cli/src/evaluation/metric.schema.ts b/packages/cli/src/evaluation.ee/metric.schema.ts similarity index 100% rename from packages/cli/src/evaluation/metric.schema.ts rename to packages/cli/src/evaluation.ee/metric.schema.ts diff --git a/packages/cli/src/evaluation/metrics.controller.ts b/packages/cli/src/evaluation.ee/metrics.controller.ts similarity index 99% rename from packages/cli/src/evaluation/metrics.controller.ts rename to packages/cli/src/evaluation.ee/metrics.controller.ts index 816228bf13..2072b978b1 100644 --- a/packages/cli/src/evaluation/metrics.controller.ts +++ b/packages/cli/src/evaluation.ee/metrics.controller.ts @@ -6,7 +6,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { testMetricCreateRequestBodySchema, testMetricPatchRequestBodySchema, -} from '@/evaluation/metric.schema'; +} from '@/evaluation.ee/metric.schema'; import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service'; import { TestDefinitionService } from './test-definition.service.ee'; diff --git a/packages/cli/src/evaluation/test-definition.schema.ts b/packages/cli/src/evaluation.ee/test-definition.schema.ts similarity index 100% rename from packages/cli/src/evaluation/test-definition.schema.ts rename to packages/cli/src/evaluation.ee/test-definition.schema.ts diff --git a/packages/cli/src/evaluation/test-definition.service.ee.ts b/packages/cli/src/evaluation.ee/test-definition.service.ee.ts similarity index 100% rename from packages/cli/src/evaluation/test-definition.service.ee.ts rename to packages/cli/src/evaluation.ee/test-definition.service.ee.ts diff --git a/packages/cli/src/evaluation/test-definitions.controller.ee.ts b/packages/cli/src/evaluation.ee/test-definitions.controller.ee.ts similarity index 97% rename from packages/cli/src/evaluation/test-definitions.controller.ee.ts rename to packages/cli/src/evaluation.ee/test-definitions.controller.ee.ts index ef4a3ed461..bd4a841948 100644 --- a/packages/cli/src/evaluation/test-definitions.controller.ee.ts +++ b/packages/cli/src/evaluation.ee/test-definitions.controller.ee.ts @@ -7,8 +7,8 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { testDefinitionCreateRequestBodySchema, testDefinitionPatchRequestBodySchema, -} from '@/evaluation/test-definition.schema'; -import { TestRunnerService } from '@/evaluation/test-runner/test-runner.service.ee'; +} from '@/evaluation.ee/test-definition.schema'; +import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee'; import { listQueryMiddleware } from '@/middlewares'; import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service'; diff --git a/packages/cli/src/evaluation/test-definitions.types.ee.ts b/packages/cli/src/evaluation.ee/test-definitions.types.ee.ts similarity index 100% rename from packages/cli/src/evaluation/test-definitions.types.ee.ts rename to packages/cli/src/evaluation.ee/test-definitions.types.ee.ts diff --git a/packages/cli/src/evaluation/test-runner/__tests__/create-pin-data.ee.test.ts b/packages/cli/src/evaluation.ee/test-runner/__tests__/create-pin-data.ee.test.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/create-pin-data.ee.test.ts rename to packages/cli/src/evaluation.ee/test-runner/__tests__/create-pin-data.ee.test.ts diff --git a/packages/cli/src/evaluation/test-runner/__tests__/evaluation-metrics.ee.test.ts b/packages/cli/src/evaluation.ee/test-runner/__tests__/evaluation-metrics.ee.test.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/evaluation-metrics.ee.test.ts rename to packages/cli/src/evaluation.ee/test-runner/__tests__/evaluation-metrics.ee.test.ts diff --git a/packages/cli/src/evaluation/test-runner/__tests__/get-start-node.ee.test.ts b/packages/cli/src/evaluation.ee/test-runner/__tests__/get-start-node.ee.test.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/get-start-node.ee.test.ts rename to packages/cli/src/evaluation.ee/test-runner/__tests__/get-start-node.ee.test.ts diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.json b/packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/execution-data.json similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.json rename to packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/execution-data.json diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json b/packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json rename to packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json b/packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json rename to packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.evaluation.json b/packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/workflow.evaluation.json similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.evaluation.json rename to packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/workflow.evaluation.json diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.multiple-triggers.json b/packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/workflow.multiple-triggers.json similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.multiple-triggers.json rename to packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/workflow.multiple-triggers.json diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.under-test.json b/packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/workflow.under-test.json similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.under-test.json rename to packages/cli/src/evaluation.ee/test-runner/__tests__/mock-data/workflow.under-test.json diff --git a/packages/cli/src/evaluation/test-runner/__tests__/test-runner.service.ee.test.ts b/packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/__tests__/test-runner.service.ee.test.ts rename to packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts diff --git a/packages/cli/src/evaluation/test-runner/evaluation-metrics.ee.ts b/packages/cli/src/evaluation.ee/test-runner/evaluation-metrics.ee.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/evaluation-metrics.ee.ts rename to packages/cli/src/evaluation.ee/test-runner/evaluation-metrics.ee.ts diff --git a/packages/cli/src/evaluation/test-runner/test-runner.service.ee.ts b/packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/test-runner.service.ee.ts rename to packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts diff --git a/packages/cli/src/evaluation/test-runner/utils.ee.ts b/packages/cli/src/evaluation.ee/test-runner/utils.ee.ts similarity index 100% rename from packages/cli/src/evaluation/test-runner/utils.ee.ts rename to packages/cli/src/evaluation.ee/test-runner/utils.ee.ts diff --git a/packages/cli/src/evaluation/test-runs.controller.ee.ts b/packages/cli/src/evaluation.ee/test-runs.controller.ee.ts similarity index 96% rename from packages/cli/src/evaluation/test-runs.controller.ee.ts rename to packages/cli/src/evaluation.ee/test-runs.controller.ee.ts index 744c420fc0..aae71376e4 100644 --- a/packages/cli/src/evaluation/test-runs.controller.ee.ts +++ b/packages/cli/src/evaluation.ee/test-runs.controller.ee.ts @@ -1,7 +1,7 @@ import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee'; import { Delete, Get, RestController } from '@/decorators'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { TestRunsRequest } from '@/evaluation/test-definitions.types.ee'; +import { TestRunsRequest } from '@/evaluation.ee/test-definitions.types.ee'; import { listQueryMiddleware } from '@/middlewares'; import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service'; diff --git a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts index a5373d0cc5..8bc967e789 100644 --- a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts +++ b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts @@ -14,7 +14,7 @@ import type { import Container from 'typedi'; import { CredentialsHelper } from '@/credentials-helper'; -import * as SecretsHelpers from '@/external-secrets/external-secrets-helper.ee'; +import * as SecretsHelpers from '@/external-secrets.ee/external-secrets-helper.ee'; import { MessageEventBusDestination } from './message-event-bus-destination.ee'; import { eventMessageGenericDestinationTestEvent } from '../event-message-classes/event-message-generic'; diff --git a/packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts b/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts similarity index 95% rename from packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts rename to packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts index 05eabd104f..341558a5fe 100644 --- a/packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts +++ b/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts @@ -3,8 +3,8 @@ import { Cipher } from 'n8n-core'; import { Container } from 'typedi'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; -import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; -import { ExternalSecretsProviders } from '@/external-secrets/external-secrets-providers.ee'; +import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; +import { ExternalSecretsProviders } from '@/external-secrets.ee/external-secrets-providers.ee'; import type { ExternalSecretsSettings } from '@/interfaces'; import { License } from '@/license'; import { diff --git a/packages/cli/src/external-secrets/constants.ts b/packages/cli/src/external-secrets.ee/constants.ts similarity index 100% rename from packages/cli/src/external-secrets/constants.ts rename to packages/cli/src/external-secrets.ee/constants.ts diff --git a/packages/cli/src/external-secrets/external-secrets-helper.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets-helper.ee.ts similarity index 100% rename from packages/cli/src/external-secrets/external-secrets-helper.ee.ts rename to packages/cli/src/external-secrets.ee/external-secrets-helper.ee.ts diff --git a/packages/cli/src/external-secrets/external-secrets-manager.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts similarity index 100% rename from packages/cli/src/external-secrets/external-secrets-manager.ee.ts rename to packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts diff --git a/packages/cli/src/external-secrets/external-secrets-providers.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets-providers.ee.ts similarity index 100% rename from packages/cli/src/external-secrets/external-secrets-providers.ee.ts rename to packages/cli/src/external-secrets.ee/external-secrets-providers.ee.ts diff --git a/packages/cli/src/external-secrets/external-secrets.controller.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets.controller.ee.ts similarity index 100% rename from packages/cli/src/external-secrets/external-secrets.controller.ee.ts rename to packages/cli/src/external-secrets.ee/external-secrets.controller.ee.ts diff --git a/packages/cli/src/external-secrets/external-secrets.service.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets.service.ee.ts similarity index 100% rename from packages/cli/src/external-secrets/external-secrets.service.ee.ts rename to packages/cli/src/external-secrets.ee/external-secrets.service.ee.ts diff --git a/packages/cli/src/external-secrets/providers/__tests__/azure-key-vault.test.ts b/packages/cli/src/external-secrets.ee/providers/__tests__/azure-key-vault.test.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/__tests__/azure-key-vault.test.ts rename to packages/cli/src/external-secrets.ee/providers/__tests__/azure-key-vault.test.ts diff --git a/packages/cli/src/external-secrets/providers/__tests__/gcp-secrets-manager.test.ts b/packages/cli/src/external-secrets.ee/providers/__tests__/gcp-secrets-manager.test.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/__tests__/gcp-secrets-manager.test.ts rename to packages/cli/src/external-secrets.ee/providers/__tests__/gcp-secrets-manager.test.ts diff --git a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-client.ts b/packages/cli/src/external-secrets.ee/providers/aws-secrets/aws-secrets-client.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-client.ts rename to packages/cli/src/external-secrets.ee/providers/aws-secrets/aws-secrets-client.ts diff --git a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts b/packages/cli/src/external-secrets.ee/providers/aws-secrets/aws-secrets-manager.ts similarity index 99% rename from packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts rename to packages/cli/src/external-secrets.ee/providers/aws-secrets/aws-secrets-manager.ts index 629b3ad626..b22c2f2436 100644 --- a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts +++ b/packages/cli/src/external-secrets.ee/providers/aws-secrets/aws-secrets-manager.ts @@ -3,7 +3,7 @@ import type { INodeProperties } from 'n8n-workflow'; import Container from 'typedi'; import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error'; -import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; +import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets.ee/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; import { AwsSecretsClient } from './aws-secrets-client'; diff --git a/packages/cli/src/external-secrets/providers/aws-secrets/types.ts b/packages/cli/src/external-secrets.ee/providers/aws-secrets/types.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/aws-secrets/types.ts rename to packages/cli/src/external-secrets.ee/providers/aws-secrets/types.ts diff --git a/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts b/packages/cli/src/external-secrets.ee/providers/azure-key-vault/azure-key-vault.ts similarity index 99% rename from packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts rename to packages/cli/src/external-secrets.ee/providers/azure-key-vault/azure-key-vault.ts index 01995d6990..a03ef468b6 100644 --- a/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts +++ b/packages/cli/src/external-secrets.ee/providers/azure-key-vault/azure-key-vault.ts @@ -4,7 +4,7 @@ import { ensureError } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow'; import Container from 'typedi'; -import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; +import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets.ee/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; import type { AzureKeyVaultContext } from './types'; diff --git a/packages/cli/src/external-secrets/providers/azure-key-vault/types.ts b/packages/cli/src/external-secrets.ee/providers/azure-key-vault/types.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/azure-key-vault/types.ts rename to packages/cli/src/external-secrets.ee/providers/azure-key-vault/types.ts diff --git a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts b/packages/cli/src/external-secrets.ee/providers/gcp-secrets-manager/gcp-secrets-manager.ts similarity index 99% rename from packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts rename to packages/cli/src/external-secrets.ee/providers/gcp-secrets-manager/gcp-secrets-manager.ts index ec29a28198..fe039fd50a 100644 --- a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts +++ b/packages/cli/src/external-secrets.ee/providers/gcp-secrets-manager/gcp-secrets-manager.ts @@ -3,7 +3,7 @@ import { Logger } from 'n8n-core'; import { ensureError, jsonParse, type INodeProperties } from 'n8n-workflow'; import Container from 'typedi'; -import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; +import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets.ee/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; import type { diff --git a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/types.ts b/packages/cli/src/external-secrets.ee/providers/gcp-secrets-manager/types.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/gcp-secrets-manager/types.ts rename to packages/cli/src/external-secrets.ee/providers/gcp-secrets-manager/types.ts diff --git a/packages/cli/src/external-secrets/providers/infisical.ts b/packages/cli/src/external-secrets.ee/providers/infisical.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/infisical.ts rename to packages/cli/src/external-secrets.ee/providers/infisical.ts diff --git a/packages/cli/src/external-secrets/providers/vault.ts b/packages/cli/src/external-secrets.ee/providers/vault.ts similarity index 100% rename from packages/cli/src/external-secrets/providers/vault.ts rename to packages/cli/src/external-secrets.ee/providers/vault.ts diff --git a/packages/cli/src/ldap/__tests__/helpers.test.ts b/packages/cli/src/ldap.ee/__tests__/helpers.test.ts similarity index 96% rename from packages/cli/src/ldap/__tests__/helpers.test.ts rename to packages/cli/src/ldap.ee/__tests__/helpers.test.ts index 5d38c58e1a..3e7a9c4b4b 100644 --- a/packages/cli/src/ldap/__tests__/helpers.test.ts +++ b/packages/cli/src/ldap.ee/__tests__/helpers.test.ts @@ -2,7 +2,7 @@ import { AuthIdentity } from '@/databases/entities/auth-identity'; import { User } from '@/databases/entities/user'; import { UserRepository } from '@/databases/repositories/user.repository'; import { generateNanoId } from '@/databases/utils/generators'; -import * as helpers from '@/ldap/helpers.ee'; +import * as helpers from '@/ldap.ee/helpers.ee'; import { mockInstance } from '@test/mocking'; const userRepository = mockInstance(UserRepository); diff --git a/packages/cli/src/ldap/constants.ts b/packages/cli/src/ldap.ee/constants.ts similarity index 100% rename from packages/cli/src/ldap/constants.ts rename to packages/cli/src/ldap.ee/constants.ts diff --git a/packages/cli/src/ldap/helpers.ee.ts b/packages/cli/src/ldap.ee/helpers.ee.ts similarity index 100% rename from packages/cli/src/ldap/helpers.ee.ts rename to packages/cli/src/ldap.ee/helpers.ee.ts diff --git a/packages/cli/src/ldap/ldap.controller.ee.ts b/packages/cli/src/ldap.ee/ldap.controller.ee.ts similarity index 100% rename from packages/cli/src/ldap/ldap.controller.ee.ts rename to packages/cli/src/ldap.ee/ldap.controller.ee.ts diff --git a/packages/cli/src/ldap/ldap.service.ee.ts b/packages/cli/src/ldap.ee/ldap.service.ee.ts similarity index 99% rename from packages/cli/src/ldap/ldap.service.ee.ts rename to packages/cli/src/ldap.ee/ldap.service.ee.ts index 1445f56f13..9a794bd5fc 100644 --- a/packages/cli/src/ldap/ldap.service.ee.ts +++ b/packages/cli/src/ldap.ee/ldap.service.ee.ts @@ -19,7 +19,7 @@ import { isEmailCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod, setCurrentAuthenticationMethod, -} from '@/sso/sso-helpers'; +} from '@/sso.ee/sso-helpers'; import { BINARY_AD_ATTRIBUTES, diff --git a/packages/cli/src/ldap/types.ts b/packages/cli/src/ldap.ee/types.ts similarity index 100% rename from packages/cli/src/ldap/types.ts rename to packages/cli/src/ldap.ee/types.ts diff --git a/packages/cli/src/permissions/check-access.ts b/packages/cli/src/permissions.ee/check-access.ts similarity index 100% rename from packages/cli/src/permissions/check-access.ts rename to packages/cli/src/permissions.ee/check-access.ts diff --git a/packages/cli/src/permissions/global-roles.ts b/packages/cli/src/permissions.ee/global-roles.ts similarity index 100% rename from packages/cli/src/permissions/global-roles.ts rename to packages/cli/src/permissions.ee/global-roles.ts diff --git a/packages/cli/src/permissions/project-roles.ts b/packages/cli/src/permissions.ee/project-roles.ts similarity index 100% rename from packages/cli/src/permissions/project-roles.ts rename to packages/cli/src/permissions.ee/project-roles.ts diff --git a/packages/cli/src/permissions/resource-roles.ts b/packages/cli/src/permissions.ee/resource-roles.ts similarity index 100% rename from packages/cli/src/permissions/resource-roles.ts rename to packages/cli/src/permissions.ee/resource-roles.ts diff --git a/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts b/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts index fdcb2f16ba..6f960980ca 100644 --- a/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts @@ -5,10 +5,10 @@ import { Container } from 'typedi'; import { getTrackingInformationFromPullResult, isSourceControlLicensed, -} from '@/environments/source-control/source-control-helper.ee'; -import { SourceControlPreferencesService } from '@/environments/source-control/source-control-preferences.service.ee'; -import { SourceControlService } from '@/environments/source-control/source-control.service.ee'; -import type { ImportResult } from '@/environments/source-control/types/import-result'; +} from '@/environments.ee/source-control/source-control-helper.ee'; +import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; +import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee'; +import type { ImportResult } from '@/environments.ee/source-control/types/import-result'; import { EventService } from '@/events/event.service'; import type { PublicSourceControlRequest } from '../../../types'; diff --git a/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts b/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts index 65fb1daab5..9e5b6dabe6 100644 --- a/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts @@ -2,7 +2,7 @@ import type { Response } from 'express'; import Container from 'typedi'; import { VariablesRepository } from '@/databases/repositories/variables.repository'; -import { VariablesController } from '@/environments/variables/variables.controller.ee'; +import { VariablesController } from '@/environments.ee/variables/variables.controller.ee'; import type { PaginatedRequest } from '@/public-api/types'; import type { VariablesRequest } from '@/requests'; diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index 7a9003dc28..500eaf0d13 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -17,7 +17,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { addNodeIds, replaceInvalidCredentials } from '@/workflow-helpers'; -import { WorkflowHistoryService } from '@/workflows/workflow-history/workflow-history.service.ee'; +import { WorkflowHistoryService } from '@/workflows/workflow-history.ee/workflow-history.service.ee'; import { WorkflowService } from '@/workflows/workflow.service'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; diff --git a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts index ed68d4761c..ace75ef610 100644 --- a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts @@ -6,7 +6,7 @@ import { Container } from 'typedi'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import type { BooleanLicenseFeature } from '@/interfaces'; import { License } from '@/license'; -import { userHasScopes } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions.ee/check-access'; import type { AuthenticatedRequest } from '@/requests'; import type { PaginatedRequest } from '../../../types'; diff --git a/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts b/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts index 4f8c8af859..b92c18c885 100644 --- a/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts +++ b/packages/cli/src/scaling/__tests__/pubsub-handler.test.ts @@ -7,7 +7,7 @@ import type { ActiveWorkflowManager } from '@/active-workflow-manager'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { EventService } from '@/events/event.service'; -import type { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; +import type { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import type { IWorkflowDb } from '@/interfaces'; import type { License } from '@/license'; import type { Push } from '@/push'; @@ -17,7 +17,7 @@ import type { TestWebhooks } from '@/webhooks/test-webhooks'; import type { Publisher } from '../pubsub/publisher.service'; import { PubSubHandler } from '../pubsub/pubsub-handler'; -import type { WorkerStatusService } from '../worker-status.service'; +import type { WorkerStatusService } from '../worker-status.service.ee'; const flushPromises = async () => await new Promise((resolve) => setImmediate(resolve)); diff --git a/packages/cli/src/scaling/pubsub/pubsub-handler.ts b/packages/cli/src/scaling/pubsub/pubsub-handler.ts index 70b5f67f72..10a763f8f1 100644 --- a/packages/cli/src/scaling/pubsub/pubsub-handler.ts +++ b/packages/cli/src/scaling/pubsub/pubsub-handler.ts @@ -7,7 +7,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { EventService } from '@/events/event.service'; import type { PubSubEventMap } from '@/events/maps/pub-sub.event-map'; -import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; +import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { License } from '@/license'; import { Push } from '@/push'; import { Publisher } from '@/scaling/pubsub/publisher.service'; @@ -16,7 +16,7 @@ import { assertNever } from '@/utils'; import { TestWebhooks } from '@/webhooks/test-webhooks'; import type { PubSub } from './pubsub.types'; -import { WorkerStatusService } from '../worker-status.service'; +import { WorkerStatusService } from '../worker-status.service.ee'; /** * Responsible for handling events emitted from messages received via a pubsub channel. diff --git a/packages/cli/src/scaling/worker-status.service.ts b/packages/cli/src/scaling/worker-status.service.ee.ts similarity index 100% rename from packages/cli/src/scaling/worker-status.service.ts rename to packages/cli/src/scaling/worker-status.service.ee.ts diff --git a/packages/cli/src/secrets-helpers.ts b/packages/cli/src/secrets-helpers.ee.ts similarity index 90% rename from packages/cli/src/secrets-helpers.ts rename to packages/cli/src/secrets-helpers.ee.ts index 88a75ae3da..fdc18c4b85 100644 --- a/packages/cli/src/secrets-helpers.ts +++ b/packages/cli/src/secrets-helpers.ee.ts @@ -1,7 +1,7 @@ import type { SecretsHelpersBase } from 'n8n-workflow'; import { Service } from 'typedi'; -import { ExternalSecretsManager } from './external-secrets/external-secrets-manager.ee'; +import { ExternalSecretsManager } from './external-secrets.ee/external-secrets-manager.ee'; @Service() export class SecretsHelper implements SecretsHelpersBase { diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 74a1311444..b2b1c3a1c8 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -23,7 +23,7 @@ import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus' import { EventService } from '@/events/event.service'; import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import type { ICredentialsOverwrite } from '@/interfaces'; -import { isLdapEnabled } from '@/ldap/helpers.ee'; +import { isLdapEnabled } from '@/ldap.ee/helpers.ee'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { handleMfaDisable, isMfaFeatureEnabled } from '@/mfa/helpers'; import { PostHogClient } from '@/posthog'; @@ -60,12 +60,12 @@ import '@/credentials/credentials.controller'; import '@/eventbus/event-bus.controller'; import '@/events/events.controller'; import '@/executions/executions.controller'; -import '@/external-secrets/external-secrets.controller.ee'; +import '@/external-secrets.ee/external-secrets.controller.ee'; import '@/license/license.controller'; -import '@/evaluation/test-definitions.controller.ee'; -import '@/evaluation/metrics.controller'; -import '@/evaluation/test-runs.controller.ee'; -import '@/workflows/workflow-history/workflow-history.controller.ee'; +import '@/evaluation.ee/test-definitions.controller.ee'; +import '@/evaluation.ee/metrics.controller'; +import '@/evaluation.ee/test-runs.controller.ee'; +import '@/workflows/workflow-history.ee/workflow-history.controller.ee'; import '@/workflows/workflows.controller'; @Service() @@ -114,8 +114,8 @@ export class Server extends AbstractServer { } if (isLdapEnabled()) { - const { LdapService } = await import('@/ldap/ldap.service.ee'); - await import('@/ldap/ldap.controller.ee'); + const { LdapService } = await import('@/ldap.ee/ldap.service.ee'); + await import('@/ldap.ee/ldap.controller.ee'); await Container.get(LdapService).init(); } @@ -142,9 +142,9 @@ export class Server extends AbstractServer { // initialize SamlService if it is licensed, even if not enabled, to // set up the initial environment try { - const { SamlService } = await import('@/sso/saml/saml.service.ee'); + const { SamlService } = await import('@/sso.ee/saml/saml.service.ee'); await Container.get(SamlService).init(); - await import('@/sso/saml/routes/saml.controller.ee'); + await import('@/sso.ee/saml/routes/saml.controller.ee'); } catch (error) { this.logger.warn(`SAML initialization failed: ${(error as Error).message}`); } @@ -154,11 +154,11 @@ export class Server extends AbstractServer { // ---------------------------------------- try { const { SourceControlService } = await import( - '@/environments/source-control/source-control.service.ee' + '@/environments.ee/source-control/source-control.service.ee' ); await Container.get(SourceControlService).init(); - await import('@/environments/source-control/source-control.controller.ee'); - await import('@/environments/variables/variables.controller.ee'); + await import('@/environments.ee/source-control/source-control.controller.ee'); + await import('@/environments.ee/variables/variables.controller.ee'); } catch (error) { this.logger.warn(`Source Control initialization failed: ${(error as Error).message}`); } diff --git a/packages/cli/src/services/__tests__/orchestration.service.test.ts b/packages/cli/src/services/__tests__/orchestration.service.test.ts index a8e72c49bf..45e79036ac 100644 --- a/packages/cli/src/services/__tests__/orchestration.service.test.ts +++ b/packages/cli/src/services/__tests__/orchestration.service.test.ts @@ -5,7 +5,7 @@ import Container from 'typedi'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import config from '@/config'; -import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; +import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { Push } from '@/push'; import { OrchestrationService } from '@/services/orchestration.service'; import { RedisClientService } from '@/services/redis-client.service'; diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 75fe36358f..ae7a596005 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -12,19 +12,19 @@ import config from '@/config'; import { inE2ETests, LICENSE_FEATURES, N8N_VERSION } from '@/constants'; import { CredentialTypes } from '@/credential-types'; import { CredentialsOverwrites } from '@/credentials-overwrites'; -import { getVariablesLimit } from '@/environments/variables/environment-helpers'; -import { getLdapLoginLabel } from '@/ldap/helpers.ee'; +import { getVariablesLimit } from '@/environments.ee/variables/environment-helpers'; +import { getLdapLoginLabel } from '@/ldap.ee/helpers.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { isApiEnabled } from '@/public-api'; import type { CommunityPackagesService } from '@/services/community-packages.service'; -import { getSamlLoginLabel } from '@/sso/saml/saml-helpers'; -import { getCurrentAuthenticationMethod } from '@/sso/sso-helpers'; +import { getSamlLoginLabel } from '@/sso.ee/saml/saml-helpers'; +import { getCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers'; import { UserManagementMailer } from '@/user-management/email'; import { getWorkflowHistoryLicensePruneTime, getWorkflowHistoryPruneTime, -} from '@/workflows/workflow-history/workflow-history-helper.ee'; +} from '@/workflows/workflow-history.ee/workflow-history-helper.ee'; import { UrlService } from './url.service'; diff --git a/packages/cli/src/services/project.service.ts b/packages/cli/src/services/project.service.ee.ts similarity index 100% rename from packages/cli/src/services/project.service.ts rename to packages/cli/src/services/project.service.ee.ts diff --git a/packages/cli/src/services/role.service.ts b/packages/cli/src/services/role.service.ts index 97adbbfb7d..a4fd5daee1 100644 --- a/packages/cli/src/services/role.service.ts +++ b/packages/cli/src/services/role.service.ts @@ -15,19 +15,19 @@ import { GLOBAL_ADMIN_SCOPES, GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES, -} from '@/permissions/global-roles'; +} from '@/permissions.ee/global-roles'; import { PERSONAL_PROJECT_OWNER_SCOPES, PROJECT_EDITOR_SCOPES, PROJECT_VIEWER_SCOPES, REGULAR_PROJECT_ADMIN_SCOPES, -} from '@/permissions/project-roles'; +} from '@/permissions.ee/project-roles'; import { CREDENTIALS_SHARING_OWNER_SCOPES, CREDENTIALS_SHARING_USER_SCOPES, WORKFLOW_SHARING_EDITOR_SCOPES, WORKFLOW_SHARING_OWNER_SCOPES, -} from '@/permissions/resource-roles'; +} from '@/permissions.ee/resource-roles'; import type { ListQuery } from '@/requests'; export type RoleNamespace = 'global' | 'project' | 'credential' | 'workflow'; diff --git a/packages/cli/src/sso/saml/__tests__/saml-helpers.test.ts b/packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts similarity index 92% rename from packages/cli/src/sso/saml/__tests__/saml-helpers.test.ts rename to packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts index 76ae2e4d50..f544e050ed 100644 --- a/packages/cli/src/sso/saml/__tests__/saml-helpers.test.ts +++ b/packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts @@ -3,8 +3,8 @@ import { User } from '@/databases/entities/user'; import { AuthIdentityRepository } from '@/databases/repositories/auth-identity.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { generateNanoId } from '@/databases/utils/generators'; -import * as helpers from '@/sso/saml/saml-helpers'; -import type { SamlUserAttributes } from '@/sso/saml/types/saml-user-attributes'; +import * as helpers from '@/sso.ee/saml/saml-helpers'; +import type { SamlUserAttributes } from '@/sso.ee/saml/types/saml-user-attributes'; import { mockInstance } from '@test/mocking'; const userRepository = mockInstance(UserRepository); diff --git a/packages/cli/src/sso/saml/__tests__/saml-validator.test.ts b/packages/cli/src/sso.ee/saml/__tests__/saml-validator.test.ts similarity index 100% rename from packages/cli/src/sso/saml/__tests__/saml-validator.test.ts rename to packages/cli/src/sso.ee/saml/__tests__/saml-validator.test.ts diff --git a/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts b/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts similarity index 97% rename from packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts rename to packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts index 708592e8e7..6070104571 100644 --- a/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts +++ b/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts @@ -5,8 +5,8 @@ import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify' import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UrlService } from '@/services/url.service'; -import * as samlHelpers from '@/sso/saml/saml-helpers'; -import { SamlService } from '@/sso/saml/saml.service.ee'; +import * as samlHelpers from '@/sso.ee/saml/saml-helpers'; +import { SamlService } from '@/sso.ee/saml/saml.service.ee'; import { mockInstance } from '@test/mocking'; import { SAML_PREFERENCES_DB_KEY } from '../constants'; diff --git a/packages/cli/src/sso/saml/constants.ts b/packages/cli/src/sso.ee/saml/constants.ts similarity index 100% rename from packages/cli/src/sso/saml/constants.ts rename to packages/cli/src/sso.ee/saml/constants.ts diff --git a/packages/cli/src/sso/saml/errors/invalid-saml-metadata.error.ts b/packages/cli/src/sso.ee/saml/errors/invalid-saml-metadata.error.ts similarity index 100% rename from packages/cli/src/sso/saml/errors/invalid-saml-metadata.error.ts rename to packages/cli/src/sso.ee/saml/errors/invalid-saml-metadata.error.ts diff --git a/packages/cli/src/sso/saml/middleware/saml-enabled-middleware.ts b/packages/cli/src/sso.ee/saml/middleware/saml-enabled-middleware.ts similarity index 100% rename from packages/cli/src/sso/saml/middleware/saml-enabled-middleware.ts rename to packages/cli/src/sso.ee/saml/middleware/saml-enabled-middleware.ts diff --git a/packages/cli/src/sso/saml/routes/__tests__/saml.controller.ee.test.ts b/packages/cli/src/sso.ee/saml/routes/__tests__/saml.controller.ee.test.ts similarity index 100% rename from packages/cli/src/sso/saml/routes/__tests__/saml.controller.ee.test.ts rename to packages/cli/src/sso.ee/saml/routes/__tests__/saml.controller.ee.test.ts diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso.ee/saml/routes/saml.controller.ee.ts similarity index 100% rename from packages/cli/src/sso/saml/routes/saml.controller.ee.ts rename to packages/cli/src/sso.ee/saml/routes/saml.controller.ee.ts diff --git a/packages/cli/src/sso/saml/saml-helpers.ts b/packages/cli/src/sso.ee/saml/saml-helpers.ts similarity index 100% rename from packages/cli/src/sso/saml/saml-helpers.ts rename to packages/cli/src/sso.ee/saml/saml-helpers.ts diff --git a/packages/cli/src/sso/saml/saml-validator.ts b/packages/cli/src/sso.ee/saml/saml-validator.ts similarity index 100% rename from packages/cli/src/sso/saml/saml-validator.ts rename to packages/cli/src/sso.ee/saml/saml-validator.ts diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso.ee/saml/saml.service.ee.ts similarity index 100% rename from packages/cli/src/sso/saml/saml.service.ee.ts rename to packages/cli/src/sso.ee/saml/saml.service.ee.ts diff --git a/packages/cli/src/sso/saml/schema/metadata-exchange.xsd.ts b/packages/cli/src/sso.ee/saml/schema/metadata-exchange.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/metadata-exchange.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/metadata-exchange.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd.ts b/packages/cli/src/sso.ee/saml/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd.ts b/packages/cli/src/sso.ee/saml/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/saml-schema-assertion-2.0.xsd.ts b/packages/cli/src/sso.ee/saml/schema/saml-schema-assertion-2.0.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/saml-schema-assertion-2.0.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/saml-schema-assertion-2.0.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/saml-schema-metadata-2.0.xsd.ts b/packages/cli/src/sso.ee/saml/schema/saml-schema-metadata-2.0.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/saml-schema-metadata-2.0.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/saml-schema-metadata-2.0.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/saml-schema-protocol-2.0.xsd.ts b/packages/cli/src/sso.ee/saml/schema/saml-schema-protocol-2.0.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/saml-schema-protocol-2.0.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/saml-schema-protocol-2.0.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/ws-addr.xsd.ts b/packages/cli/src/sso.ee/saml/schema/ws-addr.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/ws-addr.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/ws-addr.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/ws-authorization.xsd.ts b/packages/cli/src/sso.ee/saml/schema/ws-authorization.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/ws-authorization.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/ws-authorization.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/ws-federation.xsd.ts b/packages/cli/src/sso.ee/saml/schema/ws-federation.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/ws-federation.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/ws-federation.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/ws-securitypolicy-1.2.xsd.ts b/packages/cli/src/sso.ee/saml/schema/ws-securitypolicy-1.2.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/ws-securitypolicy-1.2.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/ws-securitypolicy-1.2.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/xenc-schema.xsd.ts b/packages/cli/src/sso.ee/saml/schema/xenc-schema.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/xenc-schema.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/xenc-schema.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/xml.xsd.ts b/packages/cli/src/sso.ee/saml/schema/xml.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/xml.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/xml.xsd.ts diff --git a/packages/cli/src/sso/saml/schema/xmldsig-core-schema.xsd.ts b/packages/cli/src/sso.ee/saml/schema/xmldsig-core-schema.xsd.ts similarity index 100% rename from packages/cli/src/sso/saml/schema/xmldsig-core-schema.xsd.ts rename to packages/cli/src/sso.ee/saml/schema/xmldsig-core-schema.xsd.ts diff --git a/packages/cli/src/sso/saml/service-provider.ee.ts b/packages/cli/src/sso.ee/saml/service-provider.ee.ts similarity index 100% rename from packages/cli/src/sso/saml/service-provider.ee.ts rename to packages/cli/src/sso.ee/saml/service-provider.ee.ts diff --git a/packages/cli/src/sso/saml/types/index.ts b/packages/cli/src/sso.ee/saml/types/index.ts similarity index 100% rename from packages/cli/src/sso/saml/types/index.ts rename to packages/cli/src/sso.ee/saml/types/index.ts diff --git a/packages/cli/src/sso/saml/types/requests.ts b/packages/cli/src/sso.ee/saml/types/requests.ts similarity index 100% rename from packages/cli/src/sso/saml/types/requests.ts rename to packages/cli/src/sso.ee/saml/types/requests.ts diff --git a/packages/cli/src/sso/saml/types/saml-attribute-mapping.ts b/packages/cli/src/sso.ee/saml/types/saml-attribute-mapping.ts similarity index 100% rename from packages/cli/src/sso/saml/types/saml-attribute-mapping.ts rename to packages/cli/src/sso.ee/saml/types/saml-attribute-mapping.ts diff --git a/packages/cli/src/sso/saml/types/saml-preferences.ts b/packages/cli/src/sso.ee/saml/types/saml-preferences.ts similarity index 100% rename from packages/cli/src/sso/saml/types/saml-preferences.ts rename to packages/cli/src/sso.ee/saml/types/saml-preferences.ts diff --git a/packages/cli/src/sso/saml/types/saml-user-attributes.ts b/packages/cli/src/sso.ee/saml/types/saml-user-attributes.ts similarity index 100% rename from packages/cli/src/sso/saml/types/saml-user-attributes.ts rename to packages/cli/src/sso.ee/saml/types/saml-user-attributes.ts diff --git a/packages/cli/src/sso/saml/views/init-sso-post.ts b/packages/cli/src/sso.ee/saml/views/init-sso-post.ts similarity index 100% rename from packages/cli/src/sso/saml/views/init-sso-post.ts rename to packages/cli/src/sso.ee/saml/views/init-sso-post.ts diff --git a/packages/cli/src/sso/sso-helpers.ts b/packages/cli/src/sso.ee/sso-helpers.ts similarity index 100% rename from packages/cli/src/sso/sso-helpers.ts rename to packages/cli/src/sso.ee/sso-helpers.ts diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index e8099e32cb..051ae4cf25 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -15,7 +15,7 @@ import type { IExecutionTrackProperties } from '@/interfaces'; import { License } from '@/license'; import { PostHogClient } from '@/posthog'; -import { SourceControlPreferencesService } from '../environments/source-control/source-control-preferences.service.ee'; +import { SourceControlPreferencesService } from '../environments.ee/source-control/source-control-preferences.service.ee'; type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success'; diff --git a/packages/cli/src/user-management/permission-checker.ts b/packages/cli/src/user-management/permission-checker.ts index c93d2acf91..51a6e8c6a3 100644 --- a/packages/cli/src/user-management/permission-checker.ts +++ b/packages/cli/src/user-management/permission-checker.ts @@ -4,7 +4,7 @@ import { Service } from 'typedi'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { OwnershipService } from '@/services/ownership.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; @Service() export class PermissionChecker { diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index bf97ce0a3f..69395b4ec8 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -59,7 +59,7 @@ import { } from './execution-lifecycle-hooks/shared/shared-hook-functions'; import { toSaveSettings } from './execution-lifecycle-hooks/to-save-settings'; import { TaskManager } from './runners/task-managers/task-manager'; -import { SecretsHelper } from './secrets-helpers'; +import { SecretsHelper } from './secrets-helpers.ee'; import { OwnershipService } from './services/ownership.service'; import { UrlService } from './services/url.service'; import { SubworkflowPolicyChecker } from './subworkflows/subworkflow-policy-checker.service'; diff --git a/packages/cli/src/workflow-helpers.ts b/packages/cli/src/workflow-helpers.ts index addae4e290..b49e1ae556 100644 --- a/packages/cli/src/workflow-helpers.ts +++ b/packages/cli/src/workflow-helpers.ts @@ -14,7 +14,7 @@ import { v4 as uuid } from 'uuid'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { VariablesService } from '@/environments/variables/variables.service.ee'; +import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; export function generateFailedExecutionFromError( mode: WorkflowExecuteMode, diff --git a/packages/cli/src/workflows/workflow-history/__tests__/workflow-history-helper.ee.test.ts b/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history-helper.ee.test.ts similarity index 97% rename from packages/cli/src/workflows/workflow-history/__tests__/workflow-history-helper.ee.test.ts rename to packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history-helper.ee.test.ts index ce3927f730..70e00d2c6d 100644 --- a/packages/cli/src/workflows/workflow-history/__tests__/workflow-history-helper.ee.test.ts +++ b/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history-helper.ee.test.ts @@ -1,6 +1,6 @@ import config from '@/config'; import { License } from '@/license'; -import { getWorkflowHistoryPruneTime } from '@/workflows/workflow-history/workflow-history-helper.ee'; +import { getWorkflowHistoryPruneTime } from '@/workflows/workflow-history.ee/workflow-history-helper.ee'; import { mockInstance } from '@test/mocking'; let licensePruneTime = -1; diff --git a/packages/cli/src/workflows/workflow-history/__tests__/workflow-history.service.ee.test.ts b/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts similarity index 96% rename from packages/cli/src/workflows/workflow-history/__tests__/workflow-history.service.ee.test.ts rename to packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts index a2a48587f0..b80b38eb9e 100644 --- a/packages/cli/src/workflows/workflow-history/__tests__/workflow-history.service.ee.test.ts +++ b/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts @@ -3,7 +3,7 @@ import { mockClear } from 'jest-mock-extended'; import { User } from '@/databases/entities/user'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; -import { WorkflowHistoryService } from '@/workflows/workflow-history/workflow-history.service.ee'; +import { WorkflowHistoryService } from '@/workflows/workflow-history.ee/workflow-history.service.ee'; import { mockInstance, mockLogger } from '@test/mocking'; import { getWorkflow } from '@test-integration/workflow'; @@ -24,7 +24,7 @@ const testUser = Object.assign(new User(), { }); let isWorkflowHistoryEnabled = true; -jest.mock('@/workflows/workflow-history/workflow-history-helper.ee', () => { +jest.mock('@/workflows/workflow-history.ee/workflow-history-helper.ee', () => { return { isWorkflowHistoryEnabled: jest.fn(() => isWorkflowHistoryEnabled), }; diff --git a/packages/cli/src/workflows/workflow-history/workflow-history-helper.ee.ts b/packages/cli/src/workflows/workflow-history.ee/workflow-history-helper.ee.ts similarity index 100% rename from packages/cli/src/workflows/workflow-history/workflow-history-helper.ee.ts rename to packages/cli/src/workflows/workflow-history.ee/workflow-history-helper.ee.ts diff --git a/packages/cli/src/workflows/workflow-history/workflow-history-manager.ee.ts b/packages/cli/src/workflows/workflow-history.ee/workflow-history-manager.ee.ts similarity index 100% rename from packages/cli/src/workflows/workflow-history/workflow-history-manager.ee.ts rename to packages/cli/src/workflows/workflow-history.ee/workflow-history-manager.ee.ts diff --git a/packages/cli/src/workflows/workflow-history/workflow-history.controller.ee.ts b/packages/cli/src/workflows/workflow-history.ee/workflow-history.controller.ee.ts similarity index 100% rename from packages/cli/src/workflows/workflow-history/workflow-history.controller.ee.ts rename to packages/cli/src/workflows/workflow-history.ee/workflow-history.controller.ee.ts diff --git a/packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts b/packages/cli/src/workflows/workflow-history.ee/workflow-history.service.ee.ts similarity index 100% rename from packages/cli/src/workflows/workflow-history/workflow-history.service.ee.ts rename to packages/cli/src/workflows/workflow-history.ee/workflow-history.service.ee.ts diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index debaf85073..830a3d2f98 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -19,7 +19,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { TransferWorkflowError } from '@/errors/response-errors/transfer-workflow.error'; import { OwnershipService } from '@/services/ownership.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import type { WorkflowWithSharingsAndCredentials, diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index facd372656..21f792747e 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -27,12 +27,12 @@ import { validateEntity } from '@/generic-helpers'; import { hasSharing, type ListQuery } from '@/requests'; import { OrchestrationService } from '@/services/orchestration.service'; import { OwnershipService } from '@/services/ownership.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { RoleService } from '@/services/role.service'; import { TagService } from '@/services/tag.service'; import * as WorkflowHelpers from '@/workflow-helpers'; -import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; +import { WorkflowHistoryService } from './workflow-history.ee/workflow-history.service.ee'; import { WorkflowSharingService } from './workflow-sharing.service'; @Service() diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index f097b3cab6..b12dfdce5a 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -31,14 +31,14 @@ import { License } from '@/license'; import { listQueryMiddleware } from '@/middlewares'; import * as ResponseHelper from '@/response-helper'; import { NamingService } from '@/services/naming.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { TagService } from '@/services/tag.service'; import { UserManagementMailer } from '@/user-management/email'; import * as utils from '@/utils'; import * as WorkflowHelpers from '@/workflow-helpers'; import { WorkflowExecutionService } from './workflow-execution.service'; -import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee'; +import { WorkflowHistoryService } from './workflow-history.ee/workflow-history.service.ee'; import { WorkflowRequest } from './workflow.request'; import { WorkflowService } from './workflow.service'; import { EnterpriseWorkflowService } from './workflow.service.ee'; diff --git a/packages/cli/test/integration/active-workflow-manager.test.ts b/packages/cli/test/integration/active-workflow-manager.test.ts index f724d156a8..f965efe709 100644 --- a/packages/cli/test/integration/active-workflow-manager.test.ts +++ b/packages/cli/test/integration/active-workflow-manager.test.ts @@ -12,7 +12,7 @@ import { ExecutionService } from '@/executions/execution.service'; import { ExternalHooks } from '@/external-hooks'; import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; -import { SecretsHelper } from '@/secrets-helpers'; +import { SecretsHelper } from '@/secrets-helpers.ee'; import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import { WebhookService } from '@/webhooks/webhook.service'; import * as AdditionalData from '@/workflow-execute-additional-data'; diff --git a/packages/cli/test/integration/commands/ldap/reset.test.ts b/packages/cli/test/integration/commands/ldap/reset.test.ts index ef0ab2c0d6..a199310f9c 100644 --- a/packages/cli/test/integration/commands/ldap/reset.test.ts +++ b/packages/cli/test/integration/commands/ldap/reset.test.ts @@ -7,8 +7,8 @@ import { CredentialsRepository } from '@/databases/repositories/credentials.repo import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { getLdapSynchronizations, saveLdapSynchronization } from '@/ldap/helpers.ee'; -import { LdapService } from '@/ldap/ldap.service.ee'; +import { getLdapSynchronizations, saveLdapSynchronization } from '@/ldap.ee/helpers.ee'; +import { LdapService } from '@/ldap.ee/ldap.service.ee'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { Push } from '@/push'; import { Telemetry } from '@/telemetry'; diff --git a/packages/cli/test/integration/commands/worker.cmd.test.ts b/packages/cli/test/integration/commands/worker.cmd.test.ts index e17a8d2279..f91c001e08 100644 --- a/packages/cli/test/integration/commands/worker.cmd.test.ts +++ b/packages/cli/test/integration/commands/worker.cmd.test.ts @@ -9,7 +9,7 @@ import config from '@/config'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import { ExternalHooks } from '@/external-hooks'; -import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; +import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { Push } from '@/push'; diff --git a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts index 5428cafbd4..1606de934d 100644 --- a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts @@ -8,7 +8,7 @@ import type { User } from '@/databases/entities/user'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import type { ListQuery } from '@/requests'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { UserManagementMailer } from '@/user-management/email'; import { createWorkflow, shareWorkflowWithUsers } from '@test-integration/db/workflows'; diff --git a/packages/cli/test/integration/environments/source-control-import.service.test.ts b/packages/cli/test/integration/environments/source-control-import.service.test.ts index 4d2a3d668a..3faa84b675 100644 --- a/packages/cli/test/integration/environments/source-control-import.service.test.ts +++ b/packages/cli/test/integration/environments/source-control-import.service.test.ts @@ -9,9 +9,9 @@ import Container from 'typedi'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; -import { SourceControlImportService } from '@/environments/source-control/source-control-import.service.ee'; -import type { ExportableCredential } from '@/environments/source-control/types/exportable-credential'; -import type { SourceControlledFile } from '@/environments/source-control/types/source-controlled-file'; +import { SourceControlImportService } from '@/environments.ee/source-control/source-control-import.service.ee'; +import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential'; +import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file'; import { mockInstance } from '../../shared/mocking'; import { saveCredential } from '../shared/db/credentials'; diff --git a/packages/cli/test/integration/environments/source-control.test.ts b/packages/cli/test/integration/environments/source-control.test.ts index f983b899aa..7e474e1f9a 100644 --- a/packages/cli/test/integration/environments/source-control.test.ts +++ b/packages/cli/test/integration/environments/source-control.test.ts @@ -2,9 +2,9 @@ import { Container } from 'typedi'; import config from '@/config'; import type { User } from '@/databases/entities/user'; -import { SourceControlPreferencesService } from '@/environments/source-control/source-control-preferences.service.ee'; -import { SourceControlService } from '@/environments/source-control/source-control.service.ee'; -import type { SourceControlledFile } from '@/environments/source-control/types/source-controlled-file'; +import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; +import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee'; +import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file'; import { Telemetry } from '@/telemetry'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/test/integration/evaluation/test-definitions.api.test.ts b/packages/cli/test/integration/evaluation/test-definitions.api.test.ts index fe977fbfd3..8dd778289d 100644 --- a/packages/cli/test/integration/evaluation/test-definitions.api.test.ts +++ b/packages/cli/test/integration/evaluation/test-definitions.api.test.ts @@ -5,7 +5,7 @@ import type { AnnotationTagEntity } from '@/databases/entities/annotation-tag-en import type { User } from '@/databases/entities/user'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { TestDefinitionRepository } from '@/databases/repositories/test-definition.repository.ee'; -import { TestRunnerService } from '@/evaluation/test-runner/test-runner.service.ee'; +import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee'; import { createAnnotationTags } from '@test-integration/db/executions'; import { createUserShell } from './../shared/db/users'; diff --git a/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts b/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts index c36340108e..7e01941c80 100644 --- a/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts +++ b/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts @@ -7,8 +7,8 @@ import config from '@/config'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { EventService } from '@/events/event.service'; -import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; -import { ExternalSecretsProviders } from '@/external-secrets/external-secrets-providers.ee'; +import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; +import { ExternalSecretsProviders } from '@/external-secrets.ee/external-secrets-providers.ee'; import type { ExternalSecretsSettings, SecretsProviderState } from '@/interfaces'; import { License } from '@/license'; diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index 17573f49f5..884f72315c 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -7,10 +7,13 @@ import config from '@/config'; import type { User } from '@/databases/entities/user'; import { AuthProviderSyncHistoryRepository } from '@/databases/repositories/auth-provider-sync-history.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { LDAP_DEFAULT_CONFIGURATION } from '@/ldap/constants'; -import { saveLdapSynchronization } from '@/ldap/helpers.ee'; -import { LdapService } from '@/ldap/ldap.service.ee'; -import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/sso-helpers'; +import { LDAP_DEFAULT_CONFIGURATION } from '@/ldap.ee/constants'; +import { saveLdapSynchronization } from '@/ldap.ee/helpers.ee'; +import { LdapService } from '@/ldap.ee/ldap.service.ee'; +import { + getCurrentAuthenticationMethod, + setCurrentAuthenticationMethod, +} from '@/sso.ee/sso-helpers'; import { randomEmail, randomName, uniqueId } from './../shared/random'; import { getPersonalProject } from '../shared/db/projects'; diff --git a/packages/cli/test/integration/password-reset.api.test.ts b/packages/cli/test/integration/password-reset.api.test.ts index 89d66c3f21..2be4c0f030 100644 --- a/packages/cli/test/integration/password-reset.api.test.ts +++ b/packages/cli/test/integration/password-reset.api.test.ts @@ -12,7 +12,7 @@ import { ExternalHooks } from '@/external-hooks'; import { License } from '@/license'; import { JwtService } from '@/services/jwt.service'; import { PasswordUtility } from '@/services/password.utility'; -import { setCurrentAuthenticationMethod } from '@/sso/sso-helpers'; +import { setCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers'; import { UserManagementMailer } from '@/user-management/email'; import { createUser } from './shared/db/users'; diff --git a/packages/cli/test/integration/project.service.integration.test.ts b/packages/cli/test/integration/project.service.integration.test.ts index 5d425d17ee..4c4ad6be5d 100644 --- a/packages/cli/test/integration/project.service.integration.test.ts +++ b/packages/cli/test/integration/project.service.integration.test.ts @@ -1,7 +1,7 @@ import Container from 'typedi'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { linkUserToProject, createTeamProject } from './shared/db/projects'; import { createUser } from './shared/db/users'; diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index 28f9d444da..51da846b5b 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -11,7 +11,7 @@ import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; import { ExecutionService } from '@/executions/execution.service'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { Telemetry } from '@/telemetry'; import { createTeamProject } from '@test-integration/db/projects'; diff --git a/packages/cli/test/integration/saml/saml-helpers.test.ts b/packages/cli/test/integration/saml/saml-helpers.test.ts index 6ac48ee93b..3396c0edc7 100644 --- a/packages/cli/test/integration/saml/saml-helpers.test.ts +++ b/packages/cli/test/integration/saml/saml-helpers.test.ts @@ -1,5 +1,5 @@ -import * as helpers from '@/sso/saml/saml-helpers'; -import type { SamlUserAttributes } from '@/sso/saml/types/saml-user-attributes'; +import * as helpers from '@/sso.ee/saml/saml-helpers'; +import type { SamlUserAttributes } from '@/sso.ee/saml/types/saml-user-attributes'; import { getPersonalProject } from '../shared/db/projects'; import * as testDb from '../shared/test-db'; diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index d30d57356a..247faaacba 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -1,6 +1,9 @@ import type { User } from '@/databases/entities/user'; -import { setSamlLoginEnabled } from '@/sso/saml/saml-helpers'; -import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/sso-helpers'; +import { setSamlLoginEnabled } from '@/sso.ee/saml/saml-helpers'; +import { + getCurrentAuthenticationMethod, + setCurrentAuthenticationMethod, +} from '@/sso.ee/sso-helpers'; import { sampleConfig } from './sample-metadata'; import { createOwner, createUser } from '../shared/db/users'; diff --git a/packages/cli/test/integration/services/project.service.test.ts b/packages/cli/test/integration/services/project.service.test.ts index 85393a4013..b475bc83eb 100644 --- a/packages/cli/test/integration/services/project.service.test.ts +++ b/packages/cli/test/integration/services/project.service.test.ts @@ -4,7 +4,7 @@ import Container from 'typedi'; import type { ProjectRole } from '@/databases/entities/project-relation'; import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { createMember } from '../shared/db/users'; import * as testDb from '../shared/test-db'; diff --git a/packages/cli/test/integration/shared/ldap.ts b/packages/cli/test/integration/shared/ldap.ts index 3f48cf2e3c..9f87bd9962 100644 --- a/packages/cli/test/integration/shared/ldap.ts +++ b/packages/cli/test/integration/shared/ldap.ts @@ -2,8 +2,8 @@ import { jsonParse } from 'n8n-workflow'; import Container from 'typedi'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; -import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap/constants'; -import type { LdapConfig } from '@/ldap/types'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; +import type { LdapConfig } from '@/ldap.ee/types'; export const defaultLdapConfig = { ...LDAP_DEFAULT_CONFIGURATION, diff --git a/packages/cli/test/integration/shared/utils/test-server.ts b/packages/cli/test/integration/shared/utils/test-server.ts index f99e093854..8b63320008 100644 --- a/packages/cli/test/integration/shared/utils/test-server.ts +++ b/packages/cli/test/integration/shared/utils/test-server.ts @@ -171,7 +171,7 @@ export const setupTestServer = ({ break; case 'variables': - await import('@/environments/variables/variables.controller.ee'); + await import('@/environments.ee/variables/variables.controller.ee'); break; case 'license': @@ -202,20 +202,20 @@ export const setupTestServer = ({ break; case 'ldap': - const { LdapService } = await import('@/ldap/ldap.service.ee'); - await import('@/ldap/ldap.controller.ee'); + const { LdapService } = await import('@/ldap.ee/ldap.service.ee'); + await import('@/ldap.ee/ldap.controller.ee'); testServer.license.enable('feat:ldap'); await Container.get(LdapService).init(); break; case 'saml': - const { setSamlLoginEnabled } = await import('@/sso/saml/saml-helpers'); - await import('@/sso/saml/routes/saml.controller.ee'); + const { setSamlLoginEnabled } = await import('@/sso.ee/saml/saml-helpers'); + await import('@/sso.ee/saml/routes/saml.controller.ee'); await setSamlLoginEnabled(true); break; case 'sourceControl': - await import('@/environments/source-control/source-control.controller.ee'); + await import('@/environments.ee/source-control/source-control.controller.ee'); break; case 'community-packages': @@ -247,11 +247,11 @@ export const setupTestServer = ({ break; case 'externalSecrets': - await import('@/external-secrets/external-secrets.controller.ee'); + await import('@/external-secrets.ee/external-secrets.controller.ee'); break; case 'workflowHistory': - await import('@/workflows/workflow-history/workflow-history.controller.ee'); + await import('@/workflows/workflow-history.ee/workflow-history.controller.ee'); break; case 'binaryData': @@ -279,9 +279,9 @@ export const setupTestServer = ({ break; case 'evaluation': - await import('@/evaluation/metrics.controller'); - await import('@/evaluation/test-definitions.controller.ee'); - await import('@/evaluation/test-runs.controller.ee'); + await import('@/evaluation.ee/metrics.controller'); + await import('@/evaluation.ee/test-definitions.controller.ee'); + await import('@/evaluation.ee/test-runs.controller.ee'); break; } } diff --git a/packages/cli/test/integration/variables.test.ts b/packages/cli/test/integration/variables.test.ts index 7dd8d00aae..c331da8e99 100644 --- a/packages/cli/test/integration/variables.test.ts +++ b/packages/cli/test/integration/variables.test.ts @@ -3,7 +3,7 @@ import { Container } from 'typedi'; import type { Variables } from '@/databases/entities/variables'; import { VariablesRepository } from '@/databases/repositories/variables.repository'; import { generateNanoId } from '@/databases/utils/generators'; -import { VariablesService } from '@/environments/variables/variables.service.ee'; +import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { CacheService } from '@/services/cache/cache.service'; import { createOwner, createUser } from './shared/db/users'; diff --git a/packages/cli/test/integration/workflow-history-manager.test.ts b/packages/cli/test/integration/workflow-history-manager.test.ts index 825da9fcbf..eaf5d74478 100644 --- a/packages/cli/test/integration/workflow-history-manager.test.ts +++ b/packages/cli/test/integration/workflow-history-manager.test.ts @@ -5,7 +5,7 @@ import Container from 'typedi'; import config from '@/config'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; import { License } from '@/license'; -import { WorkflowHistoryManager } from '@/workflows/workflow-history/workflow-history-manager.ee'; +import { WorkflowHistoryManager } from '@/workflows/workflow-history.ee/workflow-history-manager.ee'; import { createManyWorkflowHistoryItems } from './shared/db/workflow-history'; import { createWorkflow } from './shared/db/workflows'; diff --git a/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts b/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts index 3730d0db6c..ab02af5b43 100644 --- a/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts @@ -2,7 +2,7 @@ import Container from 'typedi'; import type { User } from '@/databases/entities/user'; import { License } from '@/license'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { WorkflowSharingService } from '@/workflows/workflow-sharing.service'; import { createUser } from '../shared/db/users'; diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index fb28918509..d0ee2f3d67 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -12,7 +12,7 @@ import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-his import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { License } from '@/license'; import type { ListQuery } from '@/requests'; -import { ProjectService } from '@/services/project.service'; +import { ProjectService } from '@/services/project.service.ee'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; import { mockInstance } from '../../shared/mocking'; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index d145ba0c63..3789372ef8 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -15,7 +15,7 @@ "strict": false, "useUnknownInCatchVariables": false }, - "include": ["src/**/*.ts", "test/**/*.ts", "src/sso/saml/saml-schema-metadata-2.0.xsd"], + "include": ["src/**/*.ts", "test/**/*.ts", "src/sso.ee/saml/saml-schema-metadata-2.0.xsd"], "references": [ { "path": "../workflow/tsconfig.build.json" }, { "path": "../core/tsconfig.build.json" }, From 1d5e891a0d3e08dcb13fc39ddf5cdb2cc854d4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 26 Dec 2024 15:31:19 +0100 Subject: [PATCH 11/66] refactor(core): Update AI-Assistant backend code to use DTOs and injectable config (no-changelog) (#12373) --- .../ai-apply-suggestion-request.dto.test.ts | 36 +++ .../ai/__tests__/ai-ask-request.dto.test.ts | 252 ++++++++++++++++++ .../ai/__tests__/ai-chat-request.dto.test.ts | 34 +++ .../dto/ai/ai-apply-suggestion-request.dto.ts | 7 + .../src/dto/ai/ai-ask-request.dto.ts | 53 ++++ .../src/dto/ai/ai-chat-request.dto.ts | 10 + packages/@n8n/api-types/src/dto/index.ts | 3 + .../config/src/configs/aiAssistant.config.ts | 8 + packages/@n8n/config/src/index.ts | 4 + packages/@n8n/config/test/config.test.ts | 3 + packages/cli/src/config/schema.ts | 9 - .../__tests__/ai.controller.test.ts | 111 ++++++++ packages/cli/src/controllers/ai.controller.ts | 27 +- packages/cli/src/requests.ts | 13 - .../src/services/__tests__/ai.service.test.ts | 132 +++++++++ packages/cli/src/services/ai.service.ts | 17 +- 16 files changed, 679 insertions(+), 40 deletions(-) create mode 100644 packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts create mode 100644 packages/@n8n/config/src/configs/aiAssistant.config.ts create mode 100644 packages/cli/src/controllers/__tests__/ai.controller.test.ts create mode 100644 packages/cli/src/services/__tests__/ai.service.test.ts diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts new file mode 100644 index 0000000000..568900e409 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts @@ -0,0 +1,36 @@ +import { AiApplySuggestionRequestDto } from '../ai-apply-suggestion-request.dto'; + +describe('AiApplySuggestionRequestDto', () => { + it('should validate a valid suggestion application request', () => { + const validRequest = { + sessionId: 'session-123', + suggestionId: 'suggestion-456', + }; + + const result = AiApplySuggestionRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should fail if sessionId is missing', () => { + const invalidRequest = { + suggestionId: 'suggestion-456', + }; + + const result = AiApplySuggestionRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['sessionId']); + }); + + it('should fail if suggestionId is missing', () => { + const invalidRequest = { + sessionId: 'session-123', + }; + + const result = AiApplySuggestionRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['suggestionId']); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts new file mode 100644 index 0000000000..a87eb5f3a4 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts @@ -0,0 +1,252 @@ +import { AiAskRequestDto } from '../ai-ask-request.dto'; + +describe('AiAskRequestDto', () => { + const validRequest = { + question: 'How can I improve this workflow?', + context: { + schema: [ + { + nodeName: 'TestNode', + schema: { + type: 'string', + key: 'testKey', + value: 'testValue', + path: '/test/path', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'inputKey', + value: [ + { + type: 'string', + key: 'nestedKey', + value: 'nestedValue', + path: '/nested/path', + }, + ], + path: '/input/path', + }, + }, + pushRef: 'push-123', + ndvPushRef: 'ndv-push-456', + }, + forNode: 'TestWorkflowNode', + }; + + it('should validate a valid AI ask request', () => { + const result = AiAskRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should fail if question is missing', () => { + const invalidRequest = { + ...validRequest, + question: undefined, + }; + + const result = AiAskRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['question']); + }); + + it('should fail if context is invalid', () => { + const invalidRequest = { + ...validRequest, + context: { + ...validRequest.context, + schema: [ + { + nodeName: 'TestNode', + schema: { + type: 'invalid-type', // Invalid type + value: 'testValue', + path: '/test/path', + }, + }, + ], + }, + }; + + const result = AiAskRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + }); + + it('should fail if forNode is missing', () => { + const invalidRequest = { + ...validRequest, + forNode: undefined, + }; + + const result = AiAskRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['forNode']); + }); + + it('should validate all possible schema types', () => { + const allTypesRequest = { + question: 'Test all possible types', + context: { + schema: [ + { + nodeName: 'AllTypesNode', + schema: { + type: 'object', + key: 'typesRoot', + value: [ + { type: 'string', key: 'stringType', value: 'string', path: '/types/string' }, + { type: 'number', key: 'numberType', value: 'number', path: '/types/number' }, + { type: 'boolean', key: 'booleanType', value: 'boolean', path: '/types/boolean' }, + { type: 'bigint', key: 'bigintType', value: 'bigint', path: '/types/bigint' }, + { type: 'symbol', key: 'symbolType', value: 'symbol', path: '/types/symbol' }, + { type: 'array', key: 'arrayType', value: [], path: '/types/array' }, + { type: 'object', key: 'objectType', value: [], path: '/types/object' }, + { + type: 'function', + key: 'functionType', + value: 'function', + path: '/types/function', + }, + { type: 'null', key: 'nullType', value: 'null', path: '/types/null' }, + { + type: 'undefined', + key: 'undefinedType', + value: 'undefined', + path: '/types/undefined', + }, + ], + path: '/types/root', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'simpleInput', + value: [ + { + type: 'string', + key: 'simpleKey', + value: 'simpleValue', + path: '/simple/path', + }, + ], + path: '/simple/input/path', + }, + }, + pushRef: 'push-types-123', + ndvPushRef: 'ndv-push-types-456', + }, + forNode: 'TypeCheckNode', + }; + + const result = AiAskRequestDto.safeParse(allTypesRequest); + expect(result.success).toBe(true); + }); + + it('should fail with invalid type', () => { + const invalidTypeRequest = { + question: 'Test invalid type', + context: { + schema: [ + { + nodeName: 'InvalidTypeNode', + schema: { + type: 'invalid-type', // This should fail + key: 'invalidKey', + value: 'invalidValue', + path: '/invalid/path', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'simpleInput', + value: [ + { + type: 'string', + key: 'simpleKey', + value: 'simpleValue', + path: '/simple/path', + }, + ], + path: '/simple/input/path', + }, + }, + pushRef: 'push-invalid-123', + ndvPushRef: 'ndv-push-invalid-456', + }, + forNode: 'InvalidTypeNode', + }; + + const result = AiAskRequestDto.safeParse(invalidTypeRequest); + expect(result.success).toBe(false); + }); + + it('should validate multiple schema entries', () => { + const multiSchemaRequest = { + question: 'Multiple schema test', + context: { + schema: [ + { + nodeName: 'FirstNode', + schema: { + type: 'string', + key: 'firstKey', + value: 'firstValue', + path: '/first/path', + }, + }, + { + nodeName: 'SecondNode', + schema: { + type: 'object', + key: 'secondKey', + value: [ + { + type: 'number', + key: 'nestedKey', + value: 'nestedValue', + path: '/second/nested/path', + }, + ], + path: '/second/path', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'simpleInput', + value: [ + { + type: 'string', + key: 'simpleKey', + value: 'simpleValue', + path: '/simple/path', + }, + ], + path: '/simple/input/path', + }, + }, + pushRef: 'push-multi-123', + ndvPushRef: 'ndv-push-multi-456', + }, + forNode: 'MultiSchemaNode', + }; + + const result = AiAskRequestDto.safeParse(multiSchemaRequest); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts new file mode 100644 index 0000000000..ce1ccffac5 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts @@ -0,0 +1,34 @@ +import { AiChatRequestDto } from '../ai-chat-request.dto'; + +describe('AiChatRequestDto', () => { + it('should validate a request with a payload and session ID', () => { + const validRequest = { + payload: { someKey: 'someValue' }, + sessionId: 'session-123', + }; + + const result = AiChatRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should validate a request with only a payload', () => { + const validRequest = { + payload: { complexObject: { nested: 'value' } }, + }; + + const result = AiChatRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should fail if payload is missing', () => { + const invalidRequest = { + sessionId: 'session-123', + }; + + const result = AiChatRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts new file mode 100644 index 0000000000..cc808dfd24 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class AiApplySuggestionRequestDto extends Z.class({ + sessionId: z.string(), + suggestionId: z.string(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts new file mode 100644 index 0000000000..9039243e05 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts @@ -0,0 +1,53 @@ +import type { AiAssistantSDK, SchemaType } from '@n8n_io/ai-assistant-sdk'; +import { z } from 'zod'; +import { Z } from 'zod-class'; + +// Note: This is copied from the sdk, since this type is not exported +type Schema = { + type: SchemaType; + key?: string; + value: string | Schema[]; + path: string; +}; + +// Create a lazy validator to handle the recursive type +const schemaValidator: z.ZodType = z.lazy(() => + z.object({ + type: z.enum([ + 'string', + 'number', + 'boolean', + 'bigint', + 'symbol', + 'array', + 'object', + 'function', + 'null', + 'undefined', + ]), + key: z.string().optional(), + value: z.union([z.string(), z.lazy(() => schemaValidator.array())]), + path: z.string(), + }), +); + +export class AiAskRequestDto + extends Z.class({ + question: z.string(), + context: z.object({ + schema: z.array( + z.object({ + nodeName: z.string(), + schema: schemaValidator, + }), + ), + inputSchema: z.object({ + nodeName: z.string(), + schema: schemaValidator, + }), + pushRef: z.string(), + ndvPushRef: z.string(), + }), + forNode: z.string(), + }) + implements AiAssistantSDK.AskAiRequestPayload {} diff --git a/packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts new file mode 100644 index 0000000000..59e7a26aa3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts @@ -0,0 +1,10 @@ +import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class AiChatRequestDto + extends Z.class({ + payload: z.object({}).passthrough(), // Allow any object shape + sessionId: z.string().optional(), + }) + implements AiAssistantSDK.ChatRequestPayload {} diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index 97d5d38459..0e57e07110 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -1,3 +1,6 @@ +export { AiAskRequestDto } from './ai/ai-ask-request.dto'; +export { AiChatRequestDto } from './ai/ai-chat-request.dto'; +export { AiApplySuggestionRequestDto } from './ai/ai-apply-suggestion-request.dto'; export { PasswordUpdateRequestDto } from './user/password-update-request.dto'; export { RoleChangeRequestDto } from './user/role-change-request.dto'; export { SettingsUpdateRequestDto } from './user/settings-update-request.dto'; diff --git a/packages/@n8n/config/src/configs/aiAssistant.config.ts b/packages/@n8n/config/src/configs/aiAssistant.config.ts new file mode 100644 index 0000000000..ff8a3986f2 --- /dev/null +++ b/packages/@n8n/config/src/configs/aiAssistant.config.ts @@ -0,0 +1,8 @@ +import { Config, Env } from '../decorators'; + +@Config +export class AiAssistantConfig { + /** Base URL of the AI assistant service */ + @Env('N8N_AI_ASSISTANT_BASE_URL') + baseUrl: string = ''; +} diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index a5144d4196..945b5f1237 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -1,3 +1,4 @@ +import { AiAssistantConfig } from './configs/aiAssistant.config'; import { CacheConfig } from './configs/cache.config'; import { CredentialsConfig } from './configs/credentials.config'; import { DatabaseConfig } from './configs/database.config'; @@ -121,4 +122,7 @@ export class GlobalConfig { @Nested diagnostics: DiagnosticsConfig; + + @Nested + aiAssistant: AiAssistantConfig; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 771d915ee4..d6d19c47fe 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -289,6 +289,9 @@ describe('GlobalConfig', () => { apiHost: 'https://ph.n8n.io', }, }, + aiAssistant: { + baseUrl: '', + }, }; it('should use all default values when no env variables are defined', () => { diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 54fa07e7f5..e8d28cb782 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -341,15 +341,6 @@ export const schema = { }, }, - aiAssistant: { - baseUrl: { - doc: 'Base URL of the AI assistant service', - format: String, - default: '', - env: 'N8N_AI_ASSISTANT_BASE_URL', - }, - }, - expression: { evaluator: { doc: 'Expression evaluator to use', diff --git a/packages/cli/src/controllers/__tests__/ai.controller.test.ts b/packages/cli/src/controllers/__tests__/ai.controller.test.ts new file mode 100644 index 0000000000..30785ae938 --- /dev/null +++ b/packages/cli/src/controllers/__tests__/ai.controller.test.ts @@ -0,0 +1,111 @@ +import type { + AiAskRequestDto, + AiApplySuggestionRequestDto, + AiChatRequestDto, +} from '@n8n/api-types'; +import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; +import { mock } from 'jest-mock-extended'; + +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import type { AuthenticatedRequest } from '@/requests'; +import type { AiService } from '@/services/ai.service'; + +import { AiController, type FlushableResponse } from '../ai.controller'; + +describe('AiController', () => { + const aiService = mock(); + const controller = new AiController(aiService); + + const request = mock({ + user: { id: 'user123' }, + }); + const response = mock(); + + beforeEach(() => { + jest.clearAllMocks(); + + response.header.mockReturnThis(); + }); + + describe('chat', () => { + const payload = mock(); + + it('should handle chat request successfully', async () => { + aiService.chat.mockResolvedValue( + mock({ + body: mock({ + pipeTo: jest.fn().mockImplementation(async (writableStream) => { + // Simulate stream writing + const writer = writableStream.getWriter(); + await writer.write(JSON.stringify({ message: 'test response' })); + await writer.close(); + }), + }), + }), + ); + + await controller.chat(request, response, payload); + + expect(aiService.chat).toHaveBeenCalledWith(payload, request.user); + expect(response.header).toHaveBeenCalledWith('Content-type', 'application/json-lines'); + expect(response.flush).toHaveBeenCalled(); + expect(response.end).toHaveBeenCalled(); + }); + + it('should throw InternalServerError if chat fails', async () => { + const mockError = new Error('Chat failed'); + + aiService.chat.mockRejectedValue(mockError); + + await expect(controller.chat(request, response, payload)).rejects.toThrow( + InternalServerError, + ); + }); + }); + + describe('applySuggestion', () => { + const payload = mock(); + + it('should apply suggestion successfully', async () => { + const clientResponse = mock(); + aiService.applySuggestion.mockResolvedValue(clientResponse); + + const result = await controller.applySuggestion(request, response, payload); + + expect(aiService.applySuggestion).toHaveBeenCalledWith(payload, request.user); + expect(result).toEqual(clientResponse); + }); + + it('should throw InternalServerError if applying suggestion fails', async () => { + const mockError = new Error('Apply suggestion failed'); + aiService.applySuggestion.mockRejectedValue(mockError); + + await expect(controller.applySuggestion(request, response, payload)).rejects.toThrow( + InternalServerError, + ); + }); + }); + + describe('askAi method', () => { + const payload = mock(); + + it('should ask AI successfully', async () => { + const clientResponse = mock(); + aiService.askAi.mockResolvedValue(clientResponse); + + const result = await controller.askAi(request, response, payload); + + expect(aiService.askAi).toHaveBeenCalledWith(payload, request.user); + expect(result).toEqual(clientResponse); + }); + + it('should throw InternalServerError if asking AI fails', async () => { + const mockError = new Error('Ask AI failed'); + aiService.askAi.mockRejectedValue(mockError); + + await expect(controller.askAi(request, response, payload)).rejects.toThrow( + InternalServerError, + ); + }); + }); +}); diff --git a/packages/cli/src/controllers/ai.controller.ts b/packages/cli/src/controllers/ai.controller.ts index be1231911a..59499112a3 100644 --- a/packages/cli/src/controllers/ai.controller.ts +++ b/packages/cli/src/controllers/ai.controller.ts @@ -1,23 +1,24 @@ +import { AiChatRequestDto, AiApplySuggestionRequestDto, AiAskRequestDto } from '@n8n/api-types'; import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; -import type { Response } from 'express'; +import { Response } from 'express'; import { strict as assert } from 'node:assert'; import { WritableStream } from 'node:stream/web'; -import { Post, RestController } from '@/decorators'; +import { Body, Post, RestController } from '@/decorators'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; -import { AiAssistantRequest } from '@/requests'; +import { AuthenticatedRequest } from '@/requests'; import { AiService } from '@/services/ai.service'; -type FlushableResponse = Response & { flush: () => void }; +export type FlushableResponse = Response & { flush: () => void }; @RestController('/ai') export class AiController { constructor(private readonly aiService: AiService) {} @Post('/chat', { rateLimit: { limit: 100 } }) - async chat(req: AiAssistantRequest.Chat, res: FlushableResponse) { + async chat(req: AuthenticatedRequest, res: FlushableResponse, @Body payload: AiChatRequestDto) { try { - const aiResponse = await this.aiService.chat(req.body, req.user); + const aiResponse = await this.aiService.chat(payload, req.user); if (aiResponse.body) { res.header('Content-type', 'application/json-lines').flush(); await aiResponse.body.pipeTo( @@ -38,10 +39,12 @@ export class AiController { @Post('/chat/apply-suggestion') async applySuggestion( - req: AiAssistantRequest.ApplySuggestionPayload, + req: AuthenticatedRequest, + _: Response, + @Body payload: AiApplySuggestionRequestDto, ): Promise { try { - return await this.aiService.applySuggestion(req.body, req.user); + return await this.aiService.applySuggestion(payload, req.user); } catch (e) { assert(e instanceof Error); throw new InternalServerError(e.message, e); @@ -49,9 +52,13 @@ export class AiController { } @Post('/ask-ai') - async askAi(req: AiAssistantRequest.AskAiPayload): Promise { + async askAi( + req: AuthenticatedRequest, + _: Response, + @Body payload: AiAskRequestDto, + ): Promise { try { - return await this.aiService.askAi(req.body, req.user); + return await this.aiService.askAi(payload, req.user); } catch (e) { assert(e instanceof Error); throw new InternalServerError(e.message, e); diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 7afb1e1bd3..f7ac415a75 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -1,5 +1,4 @@ import type { Scope } from '@n8n/permissions'; -import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; import type express from 'express'; import type { BannerName, @@ -574,15 +573,3 @@ export declare namespace NpsSurveyRequest { // once some schema validation is added type NpsSurveyUpdate = AuthenticatedRequest<{}, {}, unknown>; } - -// ---------------------------------- -// /ai-assistant -// ---------------------------------- - -export declare namespace AiAssistantRequest { - type Chat = AuthenticatedRequest<{}, {}, AiAssistantSDK.ChatRequestPayload>; - - type SuggestionPayload = { sessionId: string; suggestionId: string }; - type ApplySuggestionPayload = AuthenticatedRequest<{}, {}, SuggestionPayload>; - type AskAiPayload = AuthenticatedRequest<{}, {}, AiAssistantSDK.AskAiRequestPayload>; -} diff --git a/packages/cli/src/services/__tests__/ai.service.test.ts b/packages/cli/src/services/__tests__/ai.service.test.ts new file mode 100644 index 0000000000..dbdcaa3e71 --- /dev/null +++ b/packages/cli/src/services/__tests__/ai.service.test.ts @@ -0,0 +1,132 @@ +import type { + AiAskRequestDto, + AiApplySuggestionRequestDto, + AiChatRequestDto, +} from '@n8n/api-types'; +import type { GlobalConfig } from '@n8n/config'; +import { AiAssistantClient, type AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; +import { mock } from 'jest-mock-extended'; +import type { IUser } from 'n8n-workflow'; + +import { N8N_VERSION } from '@/constants'; +import type { License } from '@/license'; + +import { AiService } from '../ai.service'; + +jest.mock('@n8n_io/ai-assistant-sdk', () => ({ + AiAssistantClient: jest.fn(), +})); + +describe('AiService', () => { + let aiService: AiService; + + const baseUrl = 'https://ai-assistant-url.com'; + const user = mock({ id: 'user123' }); + const client = mock(); + const license = mock(); + const globalConfig = mock({ + logging: { level: 'info' }, + aiAssistant: { baseUrl }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + (AiAssistantClient as jest.Mock).mockImplementation(() => client); + aiService = new AiService(license, globalConfig); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('init', () => { + it('should not initialize client if AI assistant is not enabled', async () => { + license.isAiAssistantEnabled.mockReturnValue(false); + + await aiService.init(); + + expect(AiAssistantClient).not.toHaveBeenCalled(); + }); + + it('should initialize client when AI assistant is enabled', async () => { + license.isAiAssistantEnabled.mockReturnValue(true); + license.loadCertStr.mockResolvedValue('mock-license-cert'); + license.getConsumerId.mockReturnValue('mock-consumer-id'); + + await aiService.init(); + + expect(AiAssistantClient).toHaveBeenCalledWith({ + licenseCert: 'mock-license-cert', + consumerId: 'mock-consumer-id', + n8nVersion: N8N_VERSION, + baseUrl, + logLevel: 'info', + }); + }); + }); + + describe('chat', () => { + const payload = mock(); + + it('should call client chat method after initialization', async () => { + license.isAiAssistantEnabled.mockReturnValue(true); + const clientResponse = mock(); + client.chat.mockResolvedValue(clientResponse); + + const result = await aiService.chat(payload, user); + + expect(client.chat).toHaveBeenCalledWith(payload, { id: user.id }); + expect(result).toEqual(clientResponse); + }); + + it('should throw error if client is not initialized', async () => { + license.isAiAssistantEnabled.mockReturnValue(false); + + await expect(aiService.chat(payload, user)).rejects.toThrow('Assistant client not setup'); + }); + }); + + describe('applySuggestion', () => { + const payload = mock(); + + it('should call client applySuggestion', async () => { + license.isAiAssistantEnabled.mockReturnValue(true); + const clientResponse = mock(); + client.applySuggestion.mockResolvedValue(clientResponse); + + const result = await aiService.applySuggestion(payload, user); + + expect(client.applySuggestion).toHaveBeenCalledWith(payload, { id: user.id }); + expect(result).toEqual(clientResponse); + }); + + it('should throw error if client is not initialized', async () => { + license.isAiAssistantEnabled.mockReturnValue(false); + + await expect(aiService.applySuggestion(payload, user)).rejects.toThrow( + 'Assistant client not setup', + ); + }); + }); + + describe('askAi', () => { + const payload = mock(); + + it('should call client askAi method after initialization', async () => { + license.isAiAssistantEnabled.mockReturnValue(true); + const clientResponse = mock(); + client.askAi.mockResolvedValue(clientResponse); + + const result = await aiService.askAi(payload, user); + + expect(client.askAi).toHaveBeenCalledWith(payload, { id: user.id }); + expect(result).toEqual(clientResponse); + }); + + it('should throw error if client is not initialized', async () => { + license.isAiAssistantEnabled.mockReturnValue(false); + + await expect(aiService.askAi(payload, user)).rejects.toThrow('Assistant client not setup'); + }); + }); +}); diff --git a/packages/cli/src/services/ai.service.ts b/packages/cli/src/services/ai.service.ts index a7b07219b5..74e28ad288 100644 --- a/packages/cli/src/services/ai.service.ts +++ b/packages/cli/src/services/ai.service.ts @@ -1,12 +1,13 @@ +import type { + AiApplySuggestionRequestDto, + AiAskRequestDto, + AiChatRequestDto, +} from '@n8n/api-types'; import { GlobalConfig } from '@n8n/config'; -import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk'; import { assert, type IUser } from 'n8n-workflow'; import { Service } from 'typedi'; -import config from '@/config'; -import type { AiAssistantRequest } from '@/requests'; - import { N8N_VERSION } from '../constants'; import { License } from '../license'; @@ -27,7 +28,7 @@ export class AiService { const licenseCert = await this.licenseService.loadCertStr(); const consumerId = this.licenseService.getConsumerId(); - const baseUrl = config.get('aiAssistant.baseUrl'); + const baseUrl = this.globalConfig.aiAssistant.baseUrl; const logLevel = this.globalConfig.logging.level; this.client = new AiAssistantClient({ @@ -39,7 +40,7 @@ export class AiService { }); } - async chat(payload: AiAssistantSDK.ChatRequestPayload, user: IUser) { + async chat(payload: AiChatRequestDto, user: IUser) { if (!this.client) { await this.init(); } @@ -48,7 +49,7 @@ export class AiService { return await this.client.chat(payload, { id: user.id }); } - async applySuggestion(payload: AiAssistantRequest.SuggestionPayload, user: IUser) { + async applySuggestion(payload: AiApplySuggestionRequestDto, user: IUser) { if (!this.client) { await this.init(); } @@ -57,7 +58,7 @@ export class AiService { return await this.client.applySuggestion(payload, { id: user.id }); } - async askAi(payload: AiAssistantSDK.AskAiRequestPayload, user: IUser) { + async askAi(payload: AiAskRequestDto, user: IUser) { if (!this.client) { await this.init(); } From 371a09de968afe0a8ab6405c5c902146cf6bfa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 26 Dec 2024 16:09:42 +0100 Subject: [PATCH 12/66] refactor(core): Port 3 more controllers to use DTOs (no-changelog) (#12375) --- .../auth/__tests__/login-request.dto.test.ts | 93 ++++++++++++++ .../resolve-signup-token-query.dto.test.ts | 87 +++++++++++++ .../src/dto/auth/login-request.dto.ts | 9 ++ .../auth/resolve-signup-token-query.dto.ts | 7 ++ packages/@n8n/api-types/src/dto/index.ts | 13 ++ .../dismiss-banner-request.dto.test.ts | 64 ++++++++++ .../__tests__/owner-setup-request.dto.test.ts | 93 ++++++++++++++ .../dto/owner/dismiss-banner-request.dto.ts | 7 ++ .../src/dto/owner/owner-setup-request.dto.ts | 11 ++ .../change-password-request.dto.test.ts | 114 +++++++++++++++++ .../forgot-password-request.dto.test.ts | 47 +++++++ .../resolve-password-token-query.dto.test.ts | 42 +++++++ .../change-password-request.dto.ts | 11 ++ .../forgot-password-request.dto.ts | 6 + .../resolve-password-token-query.dto.ts | 7 ++ packages/@n8n/api-types/src/index.ts | 1 + .../src/schemas/bannerName.schema.ts | 11 ++ .../api-types/src/schemas/password.schema.ts | 16 +++ .../src/schemas/passwordResetToken.schema.ts | 3 + .../__tests__/owner.controller.test.ts | 117 ++++++++++-------- .../cli/src/controllers/auth.controller.ts | 43 +++---- .../cli/src/controllers/e2e.controller.ts | 11 +- .../cli/src/controllers/owner.controller.ts | 39 +++--- .../controllers/password-reset.controller.ts | 74 ++++------- .../cli/src/controllers/users.controller.ts | 12 +- packages/cli/src/decorators/index.ts | 2 +- .../variables/variables.controller.ee.ts | 12 +- packages/cli/src/requests.ts | 66 +--------- packages/cli/src/services/password.utility.ts | 1 + .../cli/test/integration/auth.api.test.ts | 15 +++ .../cli/test/integration/mfa/mfa.api.test.ts | 4 +- packages/editor-ui/src/Interface.ts | 2 +- packages/editor-ui/src/api/ui.ts | 2 +- .../src/components/banners/BaseBanner.vue | 2 +- packages/editor-ui/src/stores/ui.store.ts | 2 +- packages/workflow/src/Interfaces.ts | 7 -- 36 files changed, 813 insertions(+), 240 deletions(-) create mode 100644 packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/auth/login-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts create mode 100644 packages/@n8n/api-types/src/schemas/bannerName.schema.ts create mode 100644 packages/@n8n/api-types/src/schemas/password.schema.ts create mode 100644 packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts diff --git a/packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts b/packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts new file mode 100644 index 0000000000..f222f1d93e --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts @@ -0,0 +1,93 @@ +import { LoginRequestDto } from '../login-request.dto'; + +describe('LoginRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid login request', + request: { + email: 'test@example.com', + password: 'securePassword123', + mfaCode: '123456', + }, + }, + { + name: 'login request without optional MFA', + request: { + email: 'test@example.com', + password: 'securePassword123', + }, + }, + { + name: 'login request with both mfaCode and mfaRecoveryCode', + request: { + email: 'test@example.com', + password: 'securePassword123', + mfaCode: '123456', + mfaRecoveryCode: 'recovery-code-123', + }, + }, + { + name: 'login request with only mfaRecoveryCode', + request: { + email: 'test@example.com', + password: 'securePassword123', + mfaRecoveryCode: 'recovery-code-123', + }, + }, + ])('should validate $name', ({ request }) => { + const result = LoginRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email', + request: { + email: 'invalid-email', + password: 'securePassword123', + }, + expectedErrorPath: ['email'], + }, + { + name: 'empty password', + request: { + email: 'test@example.com', + password: '', + }, + expectedErrorPath: ['password'], + }, + { + name: 'missing email', + request: { + password: 'securePassword123', + }, + expectedErrorPath: ['email'], + }, + { + name: 'missing password', + request: { + email: 'test@example.com', + }, + expectedErrorPath: ['password'], + }, + { + name: 'whitespace in email and password', + request: { + email: ' test@example.com ', + password: ' securePassword123 ', + }, + expectedErrorPath: ['email'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = LoginRequestDto.safeParse(request); + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts b/packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts new file mode 100644 index 0000000000..218fe9107a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts @@ -0,0 +1,87 @@ +import { ResolveSignupTokenQueryDto } from '../resolve-signup-token-query.dto'; + +describe('ResolveSignupTokenQueryDto', () => { + const validUuid = '123e4567-e89b-12d3-a456-426614174000'; + + describe('Valid requests', () => { + test.each([ + { + name: 'standard UUID', + request: { + inviterId: validUuid, + inviteeId: validUuid, + }, + }, + ])('should validate $name', ({ request }) => { + const result = ResolveSignupTokenQueryDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid inviterId UUID', + request: { + inviterId: 'not-a-valid-uuid', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'invalid inviteeId UUID', + request: { + inviterId: validUuid, + inviteeId: 'not-a-valid-uuid', + }, + expectedErrorPath: ['inviteeId'], + }, + { + name: 'missing inviterId', + request: { + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'missing inviteeId', + request: { + inviterId: validUuid, + }, + expectedErrorPath: ['inviteeId'], + }, + { + name: 'UUID with invalid characters', + request: { + inviterId: '123e4567-e89b-12d3-a456-42661417400G', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'UUID too long', + request: { + inviterId: '123e4567-e89b-12d3-a456-426614174001234', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'UUID too short', + request: { + inviterId: '123e4567-e89b-12d3-a456', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ResolveSignupTokenQueryDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/auth/login-request.dto.ts b/packages/@n8n/api-types/src/dto/auth/login-request.dto.ts new file mode 100644 index 0000000000..894263992c --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/login-request.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class LoginRequestDto extends Z.class({ + email: z.string().email(), + password: z.string().min(1), + mfaCode: z.string().optional(), + mfaRecoveryCode: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts b/packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts new file mode 100644 index 0000000000..768202ff04 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class ResolveSignupTokenQueryDto extends Z.class({ + inviterId: z.string().uuid(), + inviteeId: z.string().uuid(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index 0e57e07110..96f55087a1 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -1,9 +1,22 @@ export { AiAskRequestDto } from './ai/ai-ask-request.dto'; export { AiChatRequestDto } from './ai/ai-chat-request.dto'; export { AiApplySuggestionRequestDto } from './ai/ai-apply-suggestion-request.dto'; + +export { LoginRequestDto } from './auth/login-request.dto'; +export { ResolveSignupTokenQueryDto } from './auth/resolve-signup-token-query.dto'; + +export { OwnerSetupRequestDto } from './owner/owner-setup-request.dto'; +export { DismissBannerRequestDto } from './owner/dismiss-banner-request.dto'; + +export { ForgotPasswordRequestDto } from './password-reset/forgot-password-request.dto'; +export { ResolvePasswordTokenQueryDto } from './password-reset/resolve-password-token-query.dto'; +export { ChangePasswordRequestDto } from './password-reset/change-password-request.dto'; + export { PasswordUpdateRequestDto } from './user/password-update-request.dto'; export { RoleChangeRequestDto } from './user/role-change-request.dto'; export { SettingsUpdateRequestDto } from './user/settings-update-request.dto'; export { UserUpdateRequestDto } from './user/user-update-request.dto'; + export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto'; + export { VariableListRequestDto } from './variables/variables-list-request.dto'; diff --git a/packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts b/packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts new file mode 100644 index 0000000000..97371de16a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts @@ -0,0 +1,64 @@ +import { bannerNameSchema } from '../../../schemas/bannerName.schema'; +import { DismissBannerRequestDto } from '../dismiss-banner-request.dto'; + +describe('DismissBannerRequestDto', () => { + describe('Valid requests', () => { + test.each( + bannerNameSchema.options.map((banner) => ({ + name: `valid banner: ${banner}`, + request: { banner }, + })), + )('should validate $name', ({ request }) => { + const result = DismissBannerRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid banner string', + request: { + banner: 'not-a-valid-banner', + }, + expectedErrorPath: ['banner'], + }, + { + name: 'non-string banner', + request: { + banner: 123, + }, + expectedErrorPath: ['banner'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = DismissBannerRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); + + describe('Optional banner', () => { + test('should validate empty request', () => { + const result = DismissBannerRequestDto.safeParse({}); + expect(result.success).toBe(true); + }); + }); + + describe('Exhaustive banner name check', () => { + test('should have all banner names defined', () => { + const expectedBanners = [ + 'V1', + 'TRIAL_OVER', + 'TRIAL', + 'NON_PRODUCTION_LICENSE', + 'EMAIL_CONFIRMATION', + ]; + + expect(bannerNameSchema.options).toEqual(expectedBanners); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts b/packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts new file mode 100644 index 0000000000..facf808ec3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts @@ -0,0 +1,93 @@ +import { OwnerSetupRequestDto } from '../owner-setup-request.dto'; + +describe('OwnerSetupRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid setup request', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + }, + ])('should validate $name', ({ request }) => { + const result = OwnerSetupRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email', + request: { + email: 'invalid-email', + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['email'], + }, + { + name: 'missing first name', + request: { + email: 'owner@example.com', + firstName: '', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['firstName'], + }, + { + name: 'missing last name', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: '', + password: 'SecurePassword123', + }, + expectedErrorPath: ['lastName'], + }, + { + name: 'password too short', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'short', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without number', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'NoNumberPassword', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without uppercase letter', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'nouppercasepassword123', + }, + expectedErrorPath: ['password'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = OwnerSetupRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts b/packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts new file mode 100644 index 0000000000..1f42381e7a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts @@ -0,0 +1,7 @@ +import { Z } from 'zod-class'; + +import { bannerNameSchema } from '../../schemas/bannerName.schema'; + +export class DismissBannerRequestDto extends Z.class({ + banner: bannerNameSchema.optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts b/packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts new file mode 100644 index 0000000000..ccaa06b18e --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { passwordSchema } from '../../schemas/password.schema'; + +export class OwnerSetupRequestDto extends Z.class({ + email: z.string().email(), + firstName: z.string().min(1, 'First name is required'), + lastName: z.string().min(1, 'Last name is required'), + password: passwordSchema, +}) {} diff --git a/packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts b/packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts new file mode 100644 index 0000000000..86b230ba5a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts @@ -0,0 +1,114 @@ +import { ChangePasswordRequestDto } from '../change-password-request.dto'; + +describe('ChangePasswordRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid password reset with token', + request: { + token: 'valid-reset-token-with-sufficient-length', + password: 'newSecurePassword123', + }, + }, + { + name: 'valid password reset with MFA code', + request: { + token: 'another-valid-reset-token', + password: 'newSecurePassword123', + mfaCode: '123456', + }, + }, + ])('should validate $name', ({ request }) => { + const result = ChangePasswordRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing token', + request: { password: 'newSecurePassword123' }, + expectedErrorPath: ['token'], + }, + { + name: 'empty token', + request: { token: '', password: 'newSecurePassword123' }, + expectedErrorPath: ['token'], + }, + { + name: 'short token', + request: { token: 'short', password: 'newSecurePassword123' }, + expectedErrorPath: ['token'], + }, + { + name: 'missing password', + request: { token: 'valid-reset-token' }, + expectedErrorPath: ['password'], + }, + { + name: 'password too short', + request: { + token: 'valid-reset-token', + password: 'short', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password too long', + request: { + token: 'valid-reset-token', + password: 'a'.repeat(65), + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without number', + request: { + token: 'valid-reset-token', + password: 'NoNumberPassword', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without uppercase letter', + request: { + token: 'valid-reset-token', + password: 'nouppercasepassword123', + }, + expectedErrorPath: ['password'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ChangePasswordRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + + describe('Edge cases', () => { + test('should handle optional MFA code correctly', () => { + const validRequest = { + token: 'valid-reset-token', + password: 'newSecurePassword123', + mfaCode: undefined, + }; + + const result = ChangePasswordRequestDto.safeParse(validRequest); + expect(result.success).toBe(true); + }); + + test('should handle token with special characters', () => { + const validRequest = { + token: 'valid-reset-token-with-special-!@#$%^&*()_+', + password: 'newSecurePassword123', + }; + + const result = ChangePasswordRequestDto.safeParse(validRequest); + expect(result.success).toBe(true); + }); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts b/packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts new file mode 100644 index 0000000000..891d52fdad --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts @@ -0,0 +1,47 @@ +import { ForgotPasswordRequestDto } from '../forgot-password-request.dto'; + +describe('ForgotPasswordRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid email', + request: { email: 'test@example.com' }, + }, + { + name: 'email with subdomain', + request: { email: 'user@sub.example.com' }, + }, + ])('should validate $name', ({ request }) => { + const result = ForgotPasswordRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email format', + request: { email: 'invalid-email' }, + expectedErrorPath: ['email'], + }, + { + name: 'missing email', + request: {}, + expectedErrorPath: ['email'], + }, + { + name: 'empty email', + request: { email: '' }, + expectedErrorPath: ['email'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ForgotPasswordRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts b/packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts new file mode 100644 index 0000000000..a2f5881ac8 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts @@ -0,0 +1,42 @@ +import { ResolvePasswordTokenQueryDto } from '../resolve-password-token-query.dto'; + +describe('ResolvePasswordTokenQueryDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid token', + request: { token: 'valid-reset-token' }, + }, + { + name: 'long token', + request: { token: 'x'.repeat(50) }, + }, + ])('should validate $name', ({ request }) => { + const result = ResolvePasswordTokenQueryDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing token', + request: {}, + expectedErrorPath: ['token'], + }, + { + name: 'empty token', + request: { token: '' }, + expectedErrorPath: ['token'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ResolvePasswordTokenQueryDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts b/packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts new file mode 100644 index 0000000000..33ef47b3f1 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { passwordSchema } from '../../schemas/password.schema'; +import { passwordResetTokenSchema } from '../../schemas/passwordResetToken.schema'; + +export class ChangePasswordRequestDto extends Z.class({ + token: passwordResetTokenSchema, + password: passwordSchema, + mfaCode: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts b/packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts new file mode 100644 index 0000000000..f6ab3cfac5 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class ForgotPasswordRequestDto extends Z.class({ + email: z.string().email(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts b/packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts new file mode 100644 index 0000000000..88385df244 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts @@ -0,0 +1,7 @@ +import { Z } from 'zod-class'; + +import { passwordResetTokenSchema } from '../../schemas/passwordResetToken.schema'; + +export class ResolvePasswordTokenQueryDto extends Z.class({ + token: passwordResetTokenSchema, +}) {} diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index d0067f7fff..b446ca3971 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -5,5 +5,6 @@ export type * from './scaling'; export type * from './frontend-settings'; export type * from './user'; +export type { BannerName } from './schemas/bannerName.schema'; export type { Collaborator } from './push/collaboration'; export type { SendWorkerStatusMessage } from './push/worker'; diff --git a/packages/@n8n/api-types/src/schemas/bannerName.schema.ts b/packages/@n8n/api-types/src/schemas/bannerName.schema.ts new file mode 100644 index 0000000000..445bc31d1a --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/bannerName.schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const bannerNameSchema = z.enum([ + 'V1', + 'TRIAL_OVER', + 'TRIAL', + 'NON_PRODUCTION_LICENSE', + 'EMAIL_CONFIRMATION', +]); + +export type BannerName = z.infer; diff --git a/packages/@n8n/api-types/src/schemas/password.schema.ts b/packages/@n8n/api-types/src/schemas/password.schema.ts new file mode 100644 index 0000000000..3c60470af7 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/password.schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +// TODO: Delete these from `cli` after all password-validation code starts using this schema +const minLength = 8; +const maxLength = 64; + +export const passwordSchema = z + .string() + .min(minLength, `Password must be ${minLength} to ${maxLength} characters long.`) + .max(maxLength, `Password must be ${minLength} to ${maxLength} characters long.`) + .refine((password) => /\d/.test(password), { + message: 'Password must contain at least 1 number.', + }) + .refine((password) => /[A-Z]/.test(password), { + message: 'Password must contain at least 1 uppercase letter.', + }); diff --git a/packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts b/packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts new file mode 100644 index 0000000000..b7c55bb886 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const passwordResetTokenSchema = z.string().min(10, 'Token too short'); diff --git a/packages/cli/src/controllers/__tests__/owner.controller.test.ts b/packages/cli/src/controllers/__tests__/owner.controller.test.ts index 0fd42aae43..b5065fa283 100644 --- a/packages/cli/src/controllers/__tests__/owner.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/owner.controller.test.ts @@ -1,7 +1,7 @@ +import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import type { Response } from 'express'; -import { anyObject, mock } from 'jest-mock-extended'; -import jwt from 'jsonwebtoken'; -import Container from 'typedi'; +import { mock } from 'jest-mock-extended'; +import type { Logger } from 'n8n-core'; import type { AuthService } from '@/auth/auth.service'; import config from '@/config'; @@ -10,27 +10,31 @@ import type { User } from '@/databases/entities/user'; import type { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { UserRepository } from '@/databases/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { License } from '@/license'; -import type { OwnerRequest } from '@/requests'; -import { PasswordUtility } from '@/services/password.utility'; +import type { EventService } from '@/events/event.service'; +import type { PublicUser } from '@/interfaces'; +import type { AuthenticatedRequest } from '@/requests'; +import type { PasswordUtility } from '@/services/password.utility'; import type { UserService } from '@/services/user.service'; -import { mockInstance } from '@test/mocking'; -import { badPasswords } from '@test/test-data'; describe('OwnerController', () => { const configGetSpy = jest.spyOn(config, 'getEnv'); + const configSetSpy = jest.spyOn(config, 'set'); + + const logger = mock(); + const eventService = mock(); const authService = mock(); const userService = mock(); const userRepository = mock(); const settingsRepository = mock(); - mockInstance(License).isWithinUsersLimit.mockReturnValue(true); + const passwordUtility = mock(); + const controller = new OwnerController( - mock(), - mock(), + logger, + eventService, settingsRepository, authService, userService, - Container.get(PasswordUtility), + passwordUtility, mock(), userRepository, ); @@ -38,38 +42,18 @@ describe('OwnerController', () => { describe('setupOwner', () => { it('should throw a BadRequestError if the instance owner is already setup', async () => { configGetSpy.mockReturnValue(true); - await expect(controller.setupOwner(mock(), mock())).rejects.toThrowError( + await expect(controller.setupOwner(mock(), mock(), mock())).rejects.toThrowError( new BadRequestError('Instance owner already setup'), ); - }); - it('should throw a BadRequestError if the email is invalid', async () => { - configGetSpy.mockReturnValue(false); - const req = mock({ body: { email: 'invalid email' } }); - await expect(controller.setupOwner(req, mock())).rejects.toThrowError( - new BadRequestError('Invalid email address'), - ); - }); - - describe('should throw if the password is invalid', () => { - Object.entries(badPasswords).forEach(([password, errorMessage]) => { - it(password, async () => { - configGetSpy.mockReturnValue(false); - const req = mock({ body: { email: 'valid@email.com', password } }); - await expect(controller.setupOwner(req, mock())).rejects.toThrowError( - new BadRequestError(errorMessage), - ); - }); - }); - }); - - it('should throw a BadRequestError if firstName & lastName are missing ', async () => { - configGetSpy.mockReturnValue(false); - const req = mock({ - body: { email: 'valid@email.com', password: 'NewPassword123', firstName: '', lastName: '' }, - }); - await expect(controller.setupOwner(req, mock())).rejects.toThrowError( - new BadRequestError('First and last names are mandatory'), + expect(userRepository.findOneOrFail).not.toHaveBeenCalled(); + expect(userRepository.save).not.toHaveBeenCalled(); + expect(authService.issueCookie).not.toHaveBeenCalled(); + expect(settingsRepository.update).not.toHaveBeenCalled(); + expect(configSetSpy).not.toHaveBeenCalled(); + expect(eventService.emit).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'Request to claim instance ownership failed because instance owner already exists', ); }); @@ -80,29 +64,52 @@ describe('OwnerController', () => { authIdentities: [], }); const browserId = 'test-browser-id'; - const req = mock({ - body: { - email: 'valid@email.com', - password: 'NewPassword123', - firstName: 'Jane', - lastName: 'Doe', - }, - user, - browserId, - }); + const req = mock({ user, browserId }); const res = mock(); + const payload = mock({ + email: 'valid@email.com', + password: 'NewPassword123', + firstName: 'Jane', + lastName: 'Doe', + }); configGetSpy.mockReturnValue(false); - userRepository.findOneOrFail.calledWith(anyObject()).mockResolvedValue(user); - userRepository.save.calledWith(anyObject()).mockResolvedValue(user); - jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); + userRepository.findOneOrFail.mockResolvedValue(user); + userRepository.save.mockResolvedValue(user); + userService.toPublic.mockResolvedValue(mock({ id: 'newUserId' })); - await controller.setupOwner(req, res); + const result = await controller.setupOwner(req, res, payload); expect(userRepository.findOneOrFail).toHaveBeenCalledWith({ where: { role: 'global:owner' }, }); expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false }); expect(authService.issueCookie).toHaveBeenCalledWith(res, user, browserId); + expect(settingsRepository.update).toHaveBeenCalledWith( + { key: 'userManagement.isInstanceOwnerSetUp' }, + { value: JSON.stringify(true) }, + ); + expect(configSetSpy).toHaveBeenCalledWith('userManagement.isInstanceOwnerSetUp', true); + expect(eventService.emit).toHaveBeenCalledWith('instance-owner-setup', { userId: 'userId' }); + expect(result.id).toEqual('newUserId'); + }); + }); + + describe('dismissBanner', () => { + it('should not call dismissBanner if no banner is provided', async () => { + const payload = mock({ banner: undefined }); + + const result = await controller.dismissBanner(mock(), mock(), payload); + + expect(settingsRepository.dismissBanner).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('should call dismissBanner with the correct banner name', async () => { + const payload = mock({ banner: 'TRIAL' }); + + await controller.dismissBanner(mock(), mock(), payload); + + expect(settingsRepository.dismissBanner).toHaveBeenCalledWith({ bannerName: 'TRIAL' }); }); }); }); diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index b012e57131..fb06c1a80b 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -1,14 +1,13 @@ +import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types'; import { Response } from 'express'; import { Logger } from 'n8n-core'; -import { ApplicationError } from 'n8n-workflow'; -import validator from 'validator'; import { handleEmailLogin, handleLdapLogin } from '@/auth'; import { AuthService } from '@/auth/auth.service'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import type { User } from '@/databases/entities/user'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { Get, Post, RestController } from '@/decorators'; +import { Body, Get, Post, Query, RestController } from '@/decorators'; import { AuthError } from '@/errors/response-errors/auth.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; @@ -17,7 +16,7 @@ import type { PublicUser } from '@/interfaces'; import { License } from '@/license'; import { MfaService } from '@/mfa/mfa.service'; import { PostHogClient } from '@/posthog'; -import { AuthenticatedRequest, LoginRequest, UserRequest } from '@/requests'; +import { AuthenticatedRequest, AuthlessRequest } from '@/requests'; import { UserService } from '@/services/user.service'; import { getCurrentAuthenticationMethod, @@ -40,10 +39,12 @@ export class AuthController { /** Log in a user */ @Post('/login', { skipAuth: true, rateLimit: true }) - async login(req: LoginRequest, res: Response): Promise { - const { email, password, mfaCode, mfaRecoveryCode } = req.body; - if (!email) throw new ApplicationError('Email is required to log in'); - if (!password) throw new ApplicationError('Password is required to log in'); + async login( + req: AuthlessRequest, + res: Response, + @Body payload: LoginRequestDto, + ): Promise { + const { email, password, mfaCode, mfaRecoveryCode } = payload; let user: User | undefined; @@ -117,8 +118,12 @@ export class AuthController { /** Validate invite token to enable invitee to set up their account */ @Get('/resolve-signup-token', { skipAuth: true }) - async resolveSignupToken(req: UserRequest.ResolveSignUp) { - const { inviterId, inviteeId } = req.query; + async resolveSignupToken( + _req: AuthlessRequest, + _res: Response, + @Query payload: ResolveSignupTokenQueryDto, + ) { + const { inviterId, inviteeId } = payload; const isWithinUsersLimit = this.license.isWithinUsersLimit(); if (!isWithinUsersLimit) { @@ -129,24 +134,6 @@ export class AuthController { throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); } - if (!inviterId || !inviteeId) { - this.logger.debug( - 'Request to resolve signup token failed because of missing user IDs in query string', - { inviterId, inviteeId }, - ); - throw new BadRequestError('Invalid payload'); - } - - // Postgres validates UUID format - for (const userId of [inviterId, inviteeId]) { - if (!validator.isUUID(userId)) { - this.logger.debug('Request to resolve signup token failed because of invalid user ID', { - userId, - }); - throw new BadRequestError('Invalid userId'); - } - } - const users = await this.userRepository.findManyByIds([inviterId, inviteeId]); if (users.length !== 2) { diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 9d0404d312..4430dfc9fa 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -17,7 +17,6 @@ import type { FeatureReturnType } from '@/license'; import { License } from '@/license'; import { MfaService } from '@/mfa/mfa.service'; import { Push } from '@/push'; -import type { UserSetupPayload } from '@/requests'; import { CacheService } from '@/services/cache/cache.service'; import { PasswordUtility } from '@/services/password.utility'; @@ -48,6 +47,16 @@ const tablesToTruncate = [ 'workflows_tags', ]; +type UserSetupPayload = { + email: string; + password: string; + firstName: string; + lastName: string; + mfaEnabled?: boolean; + mfaSecret?: string; + mfaRecoveryCodes?: string[]; +}; + type ResetRequest = Request< {}, {}, diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 1db250c488..5c7e8d1e2a 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -1,17 +1,17 @@ +import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import { Response } from 'express'; import { Logger } from 'n8n-core'; -import validator from 'validator'; import { AuthService } from '@/auth/auth.service'; import config from '@/config'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { GlobalScope, Post, RestController } from '@/decorators'; +import { Body, GlobalScope, Post, RestController } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; import { validateEntity } from '@/generic-helpers'; import { PostHogClient } from '@/posthog'; -import { OwnerRequest } from '@/requests'; +import { AuthenticatedRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; import { UserService } from '@/services/user.service'; @@ -33,8 +33,8 @@ export class OwnerController { * and enable `isInstanceOwnerSetUp` setting. */ @Post('/setup', { skipAuth: true }) - async setupOwner(req: OwnerRequest.Post, res: Response) { - const { email, firstName, lastName, password } = req.body; + async setupOwner(req: AuthenticatedRequest, res: Response, @Body payload: OwnerSetupRequestDto) { + const { email, firstName, lastName, password } = payload; if (config.getEnv('userManagement.isInstanceOwnerSetUp')) { this.logger.debug( @@ -43,31 +43,15 @@ export class OwnerController { throw new BadRequestError('Instance owner already setup'); } - if (!email || !validator.isEmail(email)) { - this.logger.debug('Request to claim instance ownership failed because of invalid email', { - invalidEmail: email, - }); - throw new BadRequestError('Invalid email address'); - } - - const validPassword = this.passwordUtility.validate(password); - - if (!firstName || !lastName) { - this.logger.debug( - 'Request to claim instance ownership failed because of missing first name or last name in payload', - { payload: req.body }, - ); - throw new BadRequestError('First and last names are mandatory'); - } - let owner = await this.userRepository.findOneOrFail({ where: { role: 'global:owner' }, }); owner.email = email; owner.firstName = firstName; owner.lastName = lastName; - owner.password = await this.passwordUtility.hash(validPassword); + owner.password = await this.passwordUtility.hash(password); + // TODO: move XSS validation out into the DTO class await validateEntity(owner); owner = await this.userRepository.save(owner, { transaction: false }); @@ -92,8 +76,13 @@ export class OwnerController { @Post('/dismiss-banner') @GlobalScope('banner:dismiss') - async dismissBanner(req: OwnerRequest.DismissBanner) { - const bannerName = 'banner' in req.body ? (req.body.banner as string) : ''; + async dismissBanner( + _req: AuthenticatedRequest, + _res: Response, + @Body payload: DismissBannerRequestDto, + ) { + const bannerName = payload.banner; + if (!bannerName) return; return await this.settingsRepository.dismissBanner({ bannerName }); } } diff --git a/packages/cli/src/controllers/password-reset.controller.ts b/packages/cli/src/controllers/password-reset.controller.ts index 7fc266c8e8..c2652aa785 100644 --- a/packages/cli/src/controllers/password-reset.controller.ts +++ b/packages/cli/src/controllers/password-reset.controller.ts @@ -1,11 +1,15 @@ +import { + ChangePasswordRequestDto, + ForgotPasswordRequestDto, + ResolvePasswordTokenQueryDto, +} from '@n8n/api-types'; import { Response } from 'express'; import { Logger } from 'n8n-core'; -import validator from 'validator'; import { AuthService } from '@/auth/auth.service'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { Get, Post, RestController } from '@/decorators'; +import { Body, Get, Post, Query, RestController } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; @@ -15,7 +19,7 @@ import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { License } from '@/license'; import { MfaService } from '@/mfa/mfa.service'; -import { PasswordResetRequest } from '@/requests'; +import { AuthlessRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; import { UserService } from '@/services/user.service'; import { isSamlCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers'; @@ -40,7 +44,11 @@ export class PasswordResetController { * Send a password reset email. */ @Post('/forgot-password', { skipAuth: true, rateLimit: { limit: 3 } }) - async forgotPassword(req: PasswordResetRequest.Email) { + async forgotPassword( + _req: AuthlessRequest, + _res: Response, + @Body payload: ForgotPasswordRequestDto, + ) { if (!this.mailer.isEmailSetUp) { this.logger.debug( 'Request to send password reset email failed because emailing was not set up', @@ -50,22 +58,7 @@ export class PasswordResetController { ); } - const { email } = req.body; - if (!email) { - this.logger.debug( - 'Request to send password reset email failed because of missing email in payload', - { payload: req.body }, - ); - throw new BadRequestError('Email is mandatory'); - } - - if (!validator.isEmail(email)) { - this.logger.debug( - 'Request to send password reset email failed because of invalid email in payload', - { invalidEmail: email }, - ); - throw new BadRequestError('Invalid email address'); - } + const { email } = payload; // User should just be able to reset password if one is already present const user = await this.userRepository.findNonShellUser(email); @@ -138,19 +131,12 @@ export class PasswordResetController { * Verify password reset token and user ID. */ @Get('/resolve-password-token', { skipAuth: true }) - async resolvePasswordToken(req: PasswordResetRequest.Credentials) { - const { token } = req.query; - - if (!token) { - this.logger.debug( - 'Request to resolve password token failed because of missing password reset token', - { - queryString: req.query, - }, - ); - throw new BadRequestError(''); - } - + async resolvePasswordToken( + _req: AuthlessRequest, + _res: Response, + @Query payload: ResolvePasswordTokenQueryDto, + ) { + const { token } = payload; const user = await this.authService.resolvePasswordResetToken(token); if (!user) throw new NotFoundError(''); @@ -170,20 +156,12 @@ export class PasswordResetController { * Verify password reset token and update password. */ @Post('/change-password', { skipAuth: true }) - async changePassword(req: PasswordResetRequest.NewPassword, res: Response) { - const { token, password, mfaCode } = req.body; - - if (!token || !password) { - this.logger.debug( - 'Request to change password failed because of missing user ID or password or reset password token in payload', - { - payload: req.body, - }, - ); - throw new BadRequestError('Missing user ID or password or reset password token'); - } - - const validPassword = this.passwordUtility.validate(password); + async changePassword( + req: AuthlessRequest, + res: Response, + @Body payload: ChangePasswordRequestDto, + ) { + const { token, password, mfaCode } = payload; const user = await this.authService.resolvePasswordResetToken(token); if (!user) throw new NotFoundError(''); @@ -198,7 +176,7 @@ export class PasswordResetController { if (!validToken) throw new BadRequestError('Invalid MFA token.'); } - const passwordHash = await this.passwordUtility.hash(validPassword); + const passwordHash = await this.passwordUtility.hash(password); await this.userService.update(user.id, { password: passwordHash }); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index e290d29463..3177c2c23b 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -11,8 +11,16 @@ import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { GlobalScope, Delete, Get, RestController, Patch, Licensed, Body } from '@/decorators'; -import { Param } from '@/decorators/args'; +import { + GlobalScope, + Delete, + Get, + RestController, + Patch, + Licensed, + Body, + Param, +} from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index bd32add475..8002bbe094 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -1,4 +1,4 @@ -export { Body } from './args'; +export { Body, Query, Param } from './args'; export { RestController } from './rest-controller'; export { Get, Post, Put, Patch, Delete } from './route'; export { Middleware } from './middleware'; diff --git a/packages/cli/src/environments.ee/variables/variables.controller.ee.ts b/packages/cli/src/environments.ee/variables/variables.controller.ee.ts index a38906b800..460d5fa009 100644 --- a/packages/cli/src/environments.ee/variables/variables.controller.ee.ts +++ b/packages/cli/src/environments.ee/variables/variables.controller.ee.ts @@ -1,7 +1,15 @@ import { VariableListRequestDto } from '@n8n/api-types'; -import { Delete, Get, GlobalScope, Licensed, Patch, Post, RestController } from '@/decorators'; -import { Query } from '@/decorators/args'; +import { + Delete, + Get, + GlobalScope, + Licensed, + Patch, + Post, + Query, + RestController, +} from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index f7ac415a75..b49c46c7ff 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -1,7 +1,6 @@ import type { Scope } from '@n8n/permissions'; import type express from 'express'; import type { - BannerName, ICredentialDataDecryptedObject, IDataObject, ILoadOptions, @@ -19,7 +18,7 @@ import type { AssignableRole, GlobalRole, User } from '@/databases/entities/user import type { Variables } from '@/databases/entities/variables'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import type { WorkflowHistory } from '@/databases/entities/workflow-history'; -import type { PublicUser, SecretsProvider, SecretsProviderState } from '@/interfaces'; +import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; import type { ProjectRole } from './databases/entities/project-relation'; import type { ScopesField } from './services/role.service'; @@ -195,42 +194,6 @@ export declare namespace MeRequest { export type SurveyAnswers = AuthenticatedRequest<{}, {}, IPersonalizationSurveyAnswersV4>; } -export interface UserSetupPayload { - email: string; - password: string; - firstName: string; - lastName: string; - mfaEnabled?: boolean; - mfaSecret?: string; - mfaRecoveryCodes?: string[]; -} - -// ---------------------------------- -// /owner -// ---------------------------------- - -export declare namespace OwnerRequest { - type Post = AuthenticatedRequest<{}, {}, UserSetupPayload, {}>; - - type DismissBanner = AuthenticatedRequest<{}, {}, Partial<{ bannerName: BannerName }>, {}>; -} - -// ---------------------------------- -// password reset endpoints -// ---------------------------------- - -export declare namespace PasswordResetRequest { - export type Email = AuthlessRequest<{}, {}, Pick>; - - export type Credentials = AuthlessRequest<{}, {}, {}, { userId?: string; token?: string }>; - - export type NewPassword = AuthlessRequest< - {}, - {}, - Pick & { token?: string; userId?: string; mfaCode?: string } - >; -} - // ---------------------------------- // /users // ---------------------------------- @@ -253,18 +216,6 @@ export declare namespace UserRequest { error?: string; }; - export type ResolveSignUp = AuthlessRequest< - {}, - {}, - {}, - { inviterId?: string; inviteeId?: string } - >; - - export type SignUp = AuthenticatedRequest< - { id: string }, - { inviterId?: string; inviteeId?: string } - >; - export type Delete = AuthenticatedRequest< { id: string; email: string; identifier: string }, {}, @@ -295,21 +246,6 @@ export declare namespace UserRequest { >; } -// ---------------------------------- -// /login -// ---------------------------------- - -export type LoginRequest = AuthlessRequest< - {}, - {}, - { - email: string; - password: string; - mfaCode?: string; - mfaRecoveryCode?: string; - } ->; - // ---------------------------------- // MFA endpoints // ---------------------------------- diff --git a/packages/cli/src/services/password.utility.ts b/packages/cli/src/services/password.utility.ts index 9719db44bb..6ae6aad61f 100644 --- a/packages/cli/src/services/password.utility.ts +++ b/packages/cli/src/services/password.utility.ts @@ -19,6 +19,7 @@ export class PasswordUtility { return await compare(plaintext, hashed); } + /** @deprecated. All input validation should move to DTOs */ validate(plaintext?: string) { if (!plaintext) throw new BadRequestError('Password is mandatory'); diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts index 6c1ddc5892..4fa2a7145c 100644 --- a/packages/cli/test/integration/auth.api.test.ts +++ b/packages/cli/test/integration/auth.api.test.ts @@ -147,6 +147,21 @@ describe('POST /login', () => { const response = await testServer.authAgentFor(ownerUser).get('/login'); expect(response.statusCode).toBe(200); }); + + test('should fail on invalid email in the payload', async () => { + const response = await testServer.authlessAgent.post('/login').send({ + email: 'invalid-email', + password: ownerPassword, + }); + + expect(response.statusCode).toBe(400); + expect(response.body).toEqual({ + validation: 'email', + code: 'invalid_string', + message: 'Invalid email', + path: ['email'], + }); + }); }); describe('GET /login', () => { diff --git a/packages/cli/test/integration/mfa/mfa.api.test.ts b/packages/cli/test/integration/mfa/mfa.api.test.ts index 498be7abd5..f69d98a74f 100644 --- a/packages/cli/test/integration/mfa/mfa.api.test.ts +++ b/packages/cli/test/integration/mfa/mfa.api.test.ts @@ -1,4 +1,4 @@ -import { randomInt, randomString } from 'n8n-workflow'; +import { randomString } from 'n8n-workflow'; import Container from 'typedi'; import { AuthService } from '@/auth/auth.service'; @@ -239,7 +239,7 @@ describe('Change password with MFA enabled', () => { .send({ password: newPassword, token: resetPasswordToken, - mfaCode: randomInt(10), + mfaCode: randomString(10), }) .expect(404); }); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index a2a1e8e065..1f799a3fa1 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -2,6 +2,7 @@ import type { Component } from 'vue'; import type { NotificationOptions as ElementNotificationOptions } from 'element-plus'; import type { Connection } from '@jsplumb/core'; import type { + BannerName, FrontendSettings, Iso8601DateTimeString, IUserManagementSettings, @@ -38,7 +39,6 @@ import type { ITelemetryTrackProperties, WorkflowSettings, IUserSettings, - BannerName, INodeExecutionData, INodeProperties, NodeConnectionType, diff --git a/packages/editor-ui/src/api/ui.ts b/packages/editor-ui/src/api/ui.ts index 5b27669e53..7716415c6a 100644 --- a/packages/editor-ui/src/api/ui.ts +++ b/packages/editor-ui/src/api/ui.ts @@ -1,6 +1,6 @@ import type { IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils/apiUtils'; -import type { BannerName } from 'n8n-workflow'; +import type { BannerName } from '@n8n/api-types'; export async function dismissBannerPermanently( context: IRestApiContext, diff --git a/packages/editor-ui/src/components/banners/BaseBanner.vue b/packages/editor-ui/src/components/banners/BaseBanner.vue index 6a0c4a343b..aac7e3428e 100644 --- a/packages/editor-ui/src/components/banners/BaseBanner.vue +++ b/packages/editor-ui/src/components/banners/BaseBanner.vue @@ -1,7 +1,7 @@ @@ -51,6 +53,7 @@ withDefaults(defineProps(), { :label="buttonText" :type="buttonType" :disabled="buttonDisabled" + :icon="buttonIcon" size="large" @click="$emit('click:button', $event)" /> diff --git a/packages/editor-ui/src/components/Logo/Logo.vue b/packages/editor-ui/src/components/Logo/Logo.vue index f10c168e28..6d643de718 100644 --- a/packages/editor-ui/src/components/Logo/Logo.vue +++ b/packages/editor-ui/src/components/Logo/Logo.vue @@ -68,6 +68,7 @@ onMounted(() => {
{{ releaseChannel }}
+
diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 40ace711e1..35f2d71f41 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -10,6 +10,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import { useVersionsStore } from '@/stores/versions.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useSourceControlStore } from '@/stores/sourceControl.store'; import { hasPermission } from '@/utils/rbac/permissions'; import { useDebounce } from '@/composables/useDebounce'; @@ -23,7 +24,7 @@ import { useBugReporting } from '@/composables/useBugReporting'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation'; -import { N8nNavigationDropdown } from 'n8n-design-system'; +import { N8nNavigationDropdown, N8nTooltip, N8nLink, N8nIconButton } from 'n8n-design-system'; import { onClickOutside, type VueInstance } from '@vueuse/core'; import Logo from './Logo/Logo.vue'; @@ -36,6 +37,7 @@ const uiStore = useUIStore(); const usersStore = useUsersStore(); const versionsStore = useVersionsStore(); const workflowsStore = useWorkflowsStore(); +const sourceControlStore = useSourceControlStore(); const { callDebounced } = useDebounce(); const externalHooks = useExternalHooks(); @@ -292,6 +294,8 @@ const { menu, handleSelect: handleMenuSelect, createProjectAppendSlotName, + createWorkflowsAppendSlotName, + createCredentialsAppendSlotName, projectsLimitReachedMessage, upgradeLabel, } = useGlobalEntityCreation(); @@ -322,7 +326,26 @@ onClickOutside(createBtn as Ref, () => { location="sidebar" :collapsed="isCollapsed" :release-channel="settingsStore.settings.releaseChannel" - /> + > + + + + + , () => { @select="handleMenuSelect" > + + diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/editor-ui/src/components/Projects/ProjectHeader.vue index 8ef45239fc..2e7ce76d70 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.vue +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.vue @@ -1,7 +1,7 @@ + + + + diff --git a/packages/design-system/src/components/N8nIconPicker/constants.ts b/packages/design-system/src/components/N8nIconPicker/constants.ts new file mode 100644 index 0000000000..ca62c03773 --- /dev/null +++ b/packages/design-system/src/components/N8nIconPicker/constants.ts @@ -0,0 +1,163 @@ +export const TEST_ICONS = [ + 'angle-double-left', + 'angle-down', + 'angle-left', + 'angle-right', + 'angle-up', + 'archive', + 'arrow-left', + 'arrow-right', + 'arrow-up', + 'arrow-down', + 'at', + 'ban', + 'balance-scale-left', + 'bars', + 'bolt', + 'book', + 'box-open', + 'bug', + 'brain', + 'calculator', + 'calendar', + 'chart-bar', + 'check', + 'check-circle', + 'check-square', + 'chevron-left', + 'chevron-right', + 'chevron-down', + 'chevron-up', + 'code', + 'code-branch', + 'cog', + 'cogs', + 'comment', + 'comments', + 'clipboard-list', + 'clock', + 'clone', + 'cloud', + 'cloud-download-alt', + 'copy', + 'cube', + 'cut', + 'database', + 'dot-circle', + 'grip-lines-vertical', + 'grip-vertical', + 'edit', + 'ellipsis-h', + 'ellipsis-v', + 'envelope', + 'equals', + 'eye', + 'exclamation-triangle', + 'expand', + 'expand-alt', + 'external-link-alt', + 'exchange-alt', + 'file', + 'file-alt', + 'file-archive', + 'file-code', + 'file-download', + 'file-export', + 'file-import', + 'file-pdf', + 'filter', + 'fingerprint', + 'flask', + 'folder-open', + 'font', + 'gift', + 'globe', + 'globe-americas', + 'graduation-cap', + 'hand-holding-usd', + 'hand-scissors', + 'handshake', + 'hand-point-left', + 'hashtag', + 'hdd', + 'history', + 'home', + 'hourglass', + 'image', + 'inbox', + 'info', + 'info-circle', + 'key', + 'language', + 'layer-group', + 'link', + 'list', + 'lightbulb', + 'lock', + 'map-signs', + 'mouse-pointer', + 'network-wired', + 'palette', + 'pause', + 'pause-circle', + 'pen', + 'pencil-alt', + 'play', + 'play-circle', + 'plug', + 'plus', + 'plus-circle', + 'plus-square', + 'project-diagram', + 'question', + 'question-circle', + 'redo', + 'remove-format', + 'robot', + 'rss', + 'save', + 'satellite-dish', + 'search', + 'search-minus', + 'search-plus', + 'server', + 'screwdriver', + 'smile', + 'sign-in-alt', + 'sign-out-alt', + 'sliders-h', + 'spinner', + 'sticky-note', + 'stop', + 'stream', + 'sun', + 'sync', + 'sync-alt', + 'table', + 'tags', + 'tasks', + 'terminal', + 'th-large', + 'thumbtack', + 'thumbs-down', + 'thumbs-up', + 'times', + 'times-circle', + 'toolbox', + 'tools', + 'trash', + 'undo', + 'unlink', + 'user', + 'user-circle', + 'user-friends', + 'users', + 'vector-square', + 'video', + 'tree', + 'user-lock', + 'gem', + 'download', + 'power-off', + 'paper-plane', +]; diff --git a/packages/design-system/src/components/N8nIconPicker/index.ts b/packages/design-system/src/components/N8nIconPicker/index.ts new file mode 100644 index 0000000000..5045eca162 --- /dev/null +++ b/packages/design-system/src/components/N8nIconPicker/index.ts @@ -0,0 +1,2 @@ +import IconPicker from './IconPicker.vue'; +export default IconPicker; diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue index e9a34c7e04..f22bcd8827 100644 --- a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -123,12 +123,17 @@ const isItemActive = (item: IMenuItem): boolean => { :disabled="item.disabled" @click="handleSelect?.(item)" > - +
+ + {{ + item.icon.value + }} +
{{ item.label }} {{ getInitials(item.label) diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts index 2d06e5b8c6..38484308ef 100644 --- a/packages/design-system/src/components/index.ts +++ b/packages/design-system/src/components/index.ts @@ -54,3 +54,4 @@ export { default as N8nUserSelect } from './N8nUserSelect'; export { default as N8nUsersList } from './N8nUsersList'; export { default as N8nResizeObserver } from './ResizeObserver'; export { N8nKeyboardShortcut } from './N8nKeyboardShortcut'; +export { default as N8nIconPicker } from './N8nIconPicker'; diff --git a/packages/design-system/src/locale/lang/en.ts b/packages/design-system/src/locale/lang/en.ts index 9e2e141b17..35f2fd9f0e 100644 --- a/packages/design-system/src/locale/lang/en.ts +++ b/packages/design-system/src/locale/lang/en.ts @@ -48,4 +48,7 @@ export default { 'assistantChat.copy': 'Copy', 'assistantChat.copied': 'Copied', 'inlineAskAssistantButton.asked': 'Asked', + 'iconPicker.button.defaultToolTip': 'Choose icon', + 'iconPicker.tabs.icons': 'Icons', + 'iconPicker.tabs.emojis': 'Emojis', } as N8nLocale; diff --git a/packages/design-system/src/types/menu.ts b/packages/design-system/src/types/menu.ts index 258ff7d555..c042489586 100644 --- a/packages/design-system/src/types/menu.ts +++ b/packages/design-system/src/types/menu.ts @@ -5,7 +5,7 @@ import type { RouteLocationRaw, RouterLinkProps } from 'vue-router'; export type IMenuItem = { id: string; label: string; - icon?: string; + icon?: string | { type: 'icon' | 'emoji'; value: string }; secondaryIcon?: { name: string; size?: 'xsmall' | 'small' | 'medium' | 'large'; diff --git a/packages/editor-ui/src/__tests__/data/projects.ts b/packages/editor-ui/src/__tests__/data/projects.ts index 98878c339b..d7c26b245e 100644 --- a/packages/editor-ui/src/__tests__/data/projects.ts +++ b/packages/editor-ui/src/__tests__/data/projects.ts @@ -10,6 +10,7 @@ import { ProjectTypes } from '@/types/projects.types'; export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({ id: faker.string.uuid(), name: faker.lorem.words({ min: 1, max: 3 }), + icon: { type: 'icon', value: 'folder' }, type: projectType ?? ProjectTypes.Personal, createdAt: faker.date.past().toISOString(), updatedAt: faker.date.recent().toISOString(), @@ -29,6 +30,7 @@ export function createTestProject(data: Partial): Project { return { id: faker.string.uuid(), name: faker.lorem.words({ min: 1, max: 3 }), + icon: { type: 'icon', value: 'folder' }, createdAt: faker.date.past().toISOString(), updatedAt: faker.date.recent().toISOString(), type: ProjectTypes.Team, diff --git a/packages/editor-ui/src/api/projects.api.ts b/packages/editor-ui/src/api/projects.api.ts index b3bae3fbf1..325feefc63 100644 --- a/packages/editor-ui/src/api/projects.api.ts +++ b/packages/editor-ui/src/api/projects.api.ts @@ -37,8 +37,8 @@ export const updateProject = async ( context: IRestApiContext, req: ProjectUpdateRequest, ): Promise => { - const { id, name, relations } = req; - await makeRestApiRequest(context, 'PATCH', `/projects/${id}`, { name, relations }); + const { id, name, icon, relations } = req; + await makeRestApiRequest(context, 'PATCH', `/projects/${id}`, { name, icon, relations }); }; export const deleteProject = async ( diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/editor-ui/src/components/Projects/ProjectHeader.vue index 2e7ce76d70..191c7d7dd5 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.vue +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.vue @@ -3,7 +3,7 @@ import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { N8nButton, N8nTooltip } from 'n8n-design-system'; import { useI18n } from '@/composables/useI18n'; -import { ProjectTypes } from '@/types/projects.types'; +import { type ProjectIcon, ProjectTypes } from '@/types/projects.types'; import { useProjectsStore } from '@/stores/projects.store'; import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import { getResourcePermissions } from '@/permissions'; @@ -17,13 +17,13 @@ const i18n = useI18n(); const projectsStore = useProjectsStore(); const sourceControlStore = useSourceControlStore(); -const headerIcon = computed(() => { +const headerIcon = computed((): ProjectIcon => { if (projectsStore.currentProject?.type === ProjectTypes.Personal) { - return 'user'; + return { type: 'icon', value: 'user' }; } else if (projectsStore.currentProject?.name) { - return 'layer-group'; + return projectsStore.currentProject.icon ?? { type: 'icon', value: 'layer-group' }; } else { - return 'home'; + return { type: 'icon', value: 'home' }; } }); @@ -107,9 +107,7 @@ const onSelect = (action: string) => {