Skip to content

Commit

Permalink
refactor(core): Continue breaking dependency cycles (no-changelog) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Feb 6, 2024
1 parent 28aee7c commit c4e3945
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 348 deletions.
331 changes: 3 additions & 328 deletions packages/cli/src/CredentialsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,43 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Service } from 'typedi';
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
import get from 'lodash/get';

import type {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialsExpressionResolveValues,
ICredentialTestFunction,
ICredentialTestRequestData,
IHttpRequestOptions,
INode,
INodeCredentialsDetails,
INodeCredentialTestResult,
INodeExecutionData,
INodeParameters,
INodeProperties,
INodeType,
IVersionedNodeType,
IRequestOptionsSimplified,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
WorkflowExecuteMode,
ITaskDataConnections,
IHttpRequestHelper,
INodeTypeData,
INodeTypes,
IWorkflowExecuteAdditionalData,
ICredentialTestFunctions,
IExecuteData,
} from 'n8n-workflow';
import {
ICredentialsHelper,
VersionedNodeType,
NodeHelpers,
RoutingNode,
Workflow,
ErrorReporterProxy as ErrorReporter,
ApplicationError,
} from 'n8n-workflow';
import { ICredentialsHelper, NodeHelpers, Workflow, ApplicationError } from 'n8n-workflow';

import type { ICredentialsDb } from '@/Interfaces';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import type { User } from '@db/entities/User';

import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { NodeTypes } from '@/NodeTypes';
import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { RESPONSE_ERROR_MESSAGES } from './constants';
import { isObjectLiteral } from './utils';

import { Logger } from '@/Logger';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { CredentialNotFoundError } from './errors/credential-not-found.error';

const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES;

const mockNode = {
name: '',
typeVersion: 1,
Expand Down Expand Up @@ -475,310 +454,6 @@ export class CredentialsHelper extends ICredentialsHelper {
await this.credentialsRepository.update(findQuery, newCredentialsData);
}

private static hasAccessToken(credentialsDecrypted: ICredentialsDecrypted) {
const oauthTokenData = credentialsDecrypted?.data?.oauthTokenData;

if (!isObjectLiteral(oauthTokenData)) return false;

return 'access_token' in oauthTokenData;
}

private getCredentialTestFunction(
credentialType: string,
): ICredentialTestFunction | ICredentialTestRequestData | undefined {
// Check if test is defined on credentials
const type = this.credentialTypes.getByName(credentialType);
if (type.test) {
return {
testRequest: type.test,
};
}

const supportedNodes = this.credentialTypes.getSupportedNodes(credentialType);
for (const nodeName of supportedNodes) {
const node = this.nodeTypes.getByName(nodeName);

// Always set to an array even if node is not versioned to not having
// to duplicate the logic
const allNodeTypes: INodeType[] = [];
if (node instanceof VersionedNodeType) {
// Node is versioned
allNodeTypes.push(...Object.values(node.nodeVersions));
} else {
// Node is not versioned
allNodeTypes.push(node as INodeType);
}

// Check each of the node versions for credential tests
for (const nodeType of allNodeTypes) {
// Check each of teh credentials
for (const { name, testedBy } of nodeType.description.credentials ?? []) {
if (
name === credentialType &&
this.credentialTypes.getParentTypes(name).includes('oAuth2Api')
) {
return async function oauth2CredTest(
this: ICredentialTestFunctions,
cred: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
return CredentialsHelper.hasAccessToken(cred)
? {
status: 'OK',
message: OAUTH2_CREDENTIAL_TEST_SUCCEEDED,
}
: {
status: 'Error',
message: OAUTH2_CREDENTIAL_TEST_FAILED,
};
};
}

if (name === credentialType && !!testedBy) {
if (typeof testedBy === 'string') {
if (node instanceof VersionedNodeType) {
// The node is versioned. So check all versions for test function
// starting with the latest
const versions = Object.keys(node.nodeVersions).sort().reverse();
for (const version of versions) {
const versionedNode = node.nodeVersions[parseInt(version, 10)];
const credentialTest = versionedNode.methods?.credentialTest;
if (credentialTest && testedBy in credentialTest) {
return credentialTest[testedBy];
}
}
}
// Test is defined as string which links to a function
return (node as unknown as INodeType).methods?.credentialTest![testedBy];
}

// Test is defined as JSON with a definition for the request to make
return {
nodeType,
testRequest: testedBy,
};
}
}
}
}

return undefined;
}

async testCredentials(
user: User,
credentialType: string,
credentialsDecrypted: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentialTestFunction = this.getCredentialTestFunction(credentialType);
if (credentialTestFunction === undefined) {
return {
status: 'Error',
message: 'No testing function found for this credential.',
};
}

if (credentialsDecrypted.data) {
try {
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
credentialsDecrypted.data = this.applyDefaultsAndOverwrites(
additionalData,
credentialsDecrypted.data,
credentialType,
'internal' as WorkflowExecuteMode,
undefined,
undefined,
user.hasGlobalScope('externalSecret:use'),
);
} catch (error) {
this.logger.debug('Credential test failed', error);
return {
status: 'Error',
message: error.message.toString(),
};
}
}

if (typeof credentialTestFunction === 'function') {
// The credentials get tested via a function that is defined on the node
const credentialTestFunctions = NodeExecuteFunctions.getCredentialTestFunctions();

return credentialTestFunction.call(credentialTestFunctions, credentialsDecrypted);
}

// Credentials get tested via request instructions

// TODO: Temp workflows get created at multiple locations (for example also LoadNodeParameterOptions),
// check if some of them are identical enough that it can be combined

let nodeType: INodeType;
if (credentialTestFunction.nodeType) {
nodeType = credentialTestFunction.nodeType;
} else {
nodeType = this.nodeTypes.getByNameAndVersion('n8n-nodes-base.noOp');
}

const node: INode = {
id: 'temp',
parameters: {},
name: 'Temp-Node',
type: nodeType.description.name,
typeVersion: Array.isArray(nodeType.description.version)
? nodeType.description.version.slice(-1)[0]
: nodeType.description.version,
position: [0, 0],
credentials: {
[credentialType]: {
id: credentialsDecrypted.id,
name: credentialsDecrypted.name,
},
},
};

const workflowData = {
nodes: [node],
connections: {},
};

const nodeTypeCopy: INodeType = {
description: {
...nodeType.description,
credentials: [
{
name: credentialType,
required: true,
},
],
properties: [
{
displayName: 'Temp',
name: 'temp',
type: 'string',
routing: {
request: credentialTestFunction.testRequest.request,
},
default: '',
},
],
},
};

mockNodesData[nodeTypeCopy.description.name] = {
sourcePath: '',
type: nodeTypeCopy,
};

const workflow = new Workflow({
nodes: workflowData.nodes,
connections: workflowData.connections,
active: false,
nodeTypes: mockNodeTypes,
});

const mode = 'internal';
const runIndex = 0;
const inputData: ITaskDataConnections = {
main: [[{ json: {} }]],
};
const connectionInputData: INodeExecutionData[] = [];
const runExecutionData: IRunExecutionData = {
resultData: {
runData: {},
},
};

const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id, node.parameters);

const routingNode = new RoutingNode(
workflow,
node,
connectionInputData,
runExecutionData ?? null,
additionalData,
mode,
);

let response: INodeExecutionData[][] | null | undefined;

try {
response = await routingNode.runNode(
inputData,
runIndex,
nodeTypeCopy,
{ node, data: {}, source: null },
NodeExecuteFunctions,
credentialsDecrypted,
);
} catch (error) {
ErrorReporter.error(error);
// Do not fail any requests to allow custom error messages and
// make logic easier
if (error.cause?.response) {
const errorResponseData = {
statusCode: error.cause.response.status,
statusMessage: error.cause.response.statusText,
};
if (credentialTestFunction.testRequest.rules) {
// Special testing rules are defined so check all in order
for (const rule of credentialTestFunction.testRequest.rules) {
if (rule.type === 'responseCode') {
if (errorResponseData.statusCode === rule.properties.value) {
return {
status: 'Error',
message: rule.properties.message,
};
}
}
}
}

if (errorResponseData.statusCode < 199 || errorResponseData.statusCode > 299) {
// All requests with response codes that are not 2xx are treated by default as failed
return {
status: 'Error',
message:
errorResponseData.statusMessage ||
`Received HTTP status code: ${errorResponseData.statusCode}`,
};
}
} else if (error.cause?.code) {
return {
status: 'Error',
message: error.cause.code,
};
}
this.logger.debug('Credential test failed', error);
return {
status: 'Error',
message: error.message.toString(),
};
} finally {
delete mockNodesData[nodeTypeCopy.description.name];
}

if (
credentialTestFunction.testRequest.rules &&
Array.isArray(credentialTestFunction.testRequest.rules)
) {
// Special testing rules are defined so check all in order
for (const rule of credentialTestFunction.testRequest.rules) {
if (rule.type === 'responseSuccessBody') {
const responseData = response![0][0].json;
if (get(responseData, rule.properties.key) === rule.properties.value) {
return {
status: 'Error',
message: rule.properties.message,
};
}
}
}
}

return {
status: 'OK',
message: 'Connection successful!',
};
}

async credentialOwnedByOwner(nodeCredential: INodeCredentialsDetails): Promise<boolean> {
if (!nodeCredential.id) {
return false;
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/InternalHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import type { GlobalRole, User } from '@db/entities/User';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MessageEventBus, type EventPayloadWorkflow } from '@/eventbus';
import type { EventPayloadWorkflow } from '@/eventbus';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
import type {
ITelemetryUserDeletionData,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import { setupAuthMiddlewares } from './middlewares';
import { isLdapEnabled } from './Ldap/helpers';
import { AbstractServer } from './AbstractServer';
import { PostHogClient } from './posthog';
import { MessageEventBus } from '@/eventbus';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { InternalHooks } from './InternalHooks';
import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee';
Expand Down
Loading

0 comments on commit c4e3945

Please sign in to comment.