diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 82f5171e41..acd2051e37 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -31,7 +31,6 @@ import express from 'express'; import { readFileSync, promises } from 'fs'; -import { readFile } from 'fs/promises'; import { exec as callbackExec } from 'child_process'; import _ from 'lodash'; import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path'; @@ -56,12 +55,9 @@ import { INodeCredentialsDetails, INodeParameters, INodePropertyOptions, - INodeType, - INodeTypeDescription, INodeTypeNameVersion, ITelemetrySettings, LoggerProxy, - NodeHelpers, WebhookHttpMethod, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -138,6 +134,7 @@ import { } from './UserManagement/UserManagementHelper'; import { loadPublicApiVersions } from './PublicApi'; import * as telemetryScripts from './telemetry/scripts'; +import { nodeTypesController } from './api/nodeTypes.api'; require('body-parser-xml')(bodyParser); @@ -816,47 +813,6 @@ class App { ), ); - // Returns all the node-types - this.app.get( - `/${this.restEndpoint}/node-types`, - ResponseHelper.send( - async (req: express.Request, res: express.Response): Promise => { - const returnData: INodeTypeDescription[] = []; - const onlyLatest = req.query.onlyLatest === 'true'; - - const nodeTypes = NodeTypes(); - const allNodes = nodeTypes.getAll(); - - const getNodeDescription = (nodeType: INodeType): INodeTypeDescription => { - const nodeInfo: INodeTypeDescription = { ...nodeType.description }; - if (req.query.includeProperties !== 'true') { - // @ts-ignore - delete nodeInfo.properties; - } - return nodeInfo; - }; - - if (onlyLatest) { - allNodes.forEach((nodeData) => { - const nodeType = NodeHelpers.getVersionedNodeType(nodeData); - const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType); - returnData.push(nodeInfo); - }); - } else { - allNodes.forEach((nodeData) => { - const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData); - allNodeTypes.forEach((element) => { - const nodeInfo: INodeTypeDescription = getNodeDescription(element); - returnData.push(nodeInfo); - }); - }); - } - - return returnData; - }, - ), - ); - this.app.get( `/${this.restEndpoint}/credential-translation`, ResponseHelper.send( @@ -878,58 +834,6 @@ class App { ), ); - // Returns node information based on node names and versions - this.app.post( - `/${this.restEndpoint}/node-types`, - ResponseHelper.send( - async (req: express.Request, res: express.Response): Promise => { - const nodeInfos = _.get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[]; - - const { defaultLocale } = this.frontendSettings; - - if (defaultLocale === 'en') { - return nodeInfos.reduce((acc, { name, version }) => { - const { description } = NodeTypes().getByNameAndVersion(name, version); - acc.push(injectCustomApiCallOption(description)); - return acc; - }, []); - } - - async function populateTranslation( - name: string, - version: number, - nodeTypes: INodeTypeDescription[], - ) { - const { description, sourcePath } = NodeTypes().getWithSourcePath(name, version); - const translationPath = await getNodeTranslationPath({ - nodeSourcePath: sourcePath, - longNodeType: description.name, - locale: defaultLocale, - }); - - try { - const translation = await readFile(translationPath, 'utf8'); - description.translation = JSON.parse(translation); - } catch (error) { - // ignore - no translation exists at path - } - - nodeTypes.push(injectCustomApiCallOption(description)); - } - - const nodeTypes: INodeTypeDescription[] = []; - - const promises = nodeInfos.map(async ({ name, version }) => - populateTranslation(name, version, nodeTypes), - ); - - await Promise.all(promises); - - return nodeTypes; - }, - ), - ); - // Returns node information based on node names and versions this.app.get( `/${this.restEndpoint}/node-translation-headers`, @@ -957,6 +861,8 @@ class App { // Node-Types // ---------------------------------------- + this.app.use(`/${this.restEndpoint}/node-types`, nodeTypesController); + // Returns the node icon this.app.get( [ @@ -1881,58 +1787,3 @@ export async function start(): Promise { } }); } - -const CUSTOM_API_CALL_NAME = 'Custom API Call'; -const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; - -/** - * Inject a `Custom API Call` option into `resource` and `operation` - * parameters in a node that supports proxy auth. - */ -function injectCustomApiCallOption(description: INodeTypeDescription) { - if (!supportsProxyAuth(description)) return description; - - description.properties.forEach((p) => { - if ( - ['resource', 'operation'].includes(p.name) && - Array.isArray(p.options) && - p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME - ) { - p.options.push({ - name: CUSTOM_API_CALL_NAME, - value: CUSTOM_API_CALL_KEY, - }); - } - - return p; - }); - - return description; -} - -const credentialTypes = CredentialTypes(); - -/** - * Whether any of the node's credential types may be used to - * make a request from a node other than itself. - */ -function supportsProxyAuth(description: INodeTypeDescription) { - if (!description.credentials) return false; - - return description.credentials.some(({ name }) => { - const credType = credentialTypes.getByName(name); - - if (credType.authenticate !== undefined) return true; - - return isOAuth(credType); - }); -} - -function isOAuth(credType: ICredentialType) { - return ( - Array.isArray(credType.extends) && - credType.extends.some((parentType) => - ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), - ) - ); -} diff --git a/packages/cli/src/api/executions.api.ts b/packages/cli/src/api/executions.api.ts index 9fa6687fd2..1e9d381e17 100644 --- a/packages/cli/src/api/executions.api.ts +++ b/packages/cli/src/api/executions.api.ts @@ -26,9 +26,9 @@ import { NodeTypes, WorkflowRunner, ResponseHelper, + IExecutionFlattedDb, } from '..'; import * as config from '../../config'; -import { ExecutionEntity } from '../databases/entities/ExecutionEntity'; import { User } from '../databases/entities/User'; import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT } from '../GenericHelpers'; import { getLogger } from '../Logger'; @@ -137,7 +137,7 @@ executionsController.get( const sharedWorkflowIds = await getSharedWorkflowIds(req.user); - const findOptions: FindManyOptions = { + const findOptions: FindManyOptions = { select: [ 'id', 'finished', diff --git a/packages/cli/src/api/nodeTypes.api.ts b/packages/cli/src/api/nodeTypes.api.ts new file mode 100644 index 0000000000..20e92966e4 --- /dev/null +++ b/packages/cli/src/api/nodeTypes.api.ts @@ -0,0 +1,164 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/no-cycle */ +import express from 'express'; +import { readFile } from 'fs/promises'; +import _ from 'lodash'; + +import { + ICredentialType, + INodeType, + INodeTypeDescription, + INodeTypeNameVersion, + NodeHelpers, +} from 'n8n-workflow'; + +import { CredentialTypes, NodeTypes, ResponseHelper } from '..'; +import config from '../../config'; +import { getNodeTranslationPath } from '../TranslationHelpers'; + +function isOAuth(credType: ICredentialType) { + return ( + Array.isArray(credType.extends) && + credType.extends.some((parentType) => + ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), + ) + ); +} + +/** + * Whether any of the node's credential types may be used to + * make a request from a node other than itself. + */ +function supportsProxyAuth(description: INodeTypeDescription) { + if (!description.credentials) return false; + + const credentialTypes = CredentialTypes(); + + return description.credentials.some(({ name }) => { + const credType = credentialTypes.getByName(name); + + if (credType.authenticate !== undefined) return true; + + return isOAuth(credType); + }); +} + +const CUSTOM_API_CALL_NAME = 'Custom API Call'; +const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; + +/** + * Inject a `Custom API Call` option into `resource` and `operation` + * parameters in a node that supports proxy auth. + */ +function injectCustomApiCallOption(description: INodeTypeDescription) { + if (!supportsProxyAuth(description)) return description; + + description.properties.forEach((p) => { + if ( + ['resource', 'operation'].includes(p.name) && + Array.isArray(p.options) && + p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME + ) { + p.options.push({ + name: CUSTOM_API_CALL_NAME, + value: CUSTOM_API_CALL_KEY, + }); + } + + return p; + }); + + return description; +} + +export const nodeTypesController = express.Router(); + +// Returns all the node-types +nodeTypesController.get( + '/', + ResponseHelper.send(async (req: express.Request): Promise => { + const returnData: INodeTypeDescription[] = []; + const onlyLatest = req.query.onlyLatest === 'true'; + + const nodeTypes = NodeTypes(); + const allNodes = nodeTypes.getAll(); + + const getNodeDescription = (nodeType: INodeType): INodeTypeDescription => { + const nodeInfo: INodeTypeDescription = { ...nodeType.description }; + if (req.query.includeProperties !== 'true') { + // @ts-ignore + delete nodeInfo.properties; + } + return nodeInfo; + }; + + if (onlyLatest) { + allNodes.forEach((nodeData) => { + const nodeType = NodeHelpers.getVersionedNodeType(nodeData); + const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType); + returnData.push(nodeInfo); + }); + } else { + allNodes.forEach((nodeData) => { + const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData); + allNodeTypes.forEach((element) => { + const nodeInfo: INodeTypeDescription = getNodeDescription(element); + returnData.push(nodeInfo); + }); + }); + } + + return returnData; + }), +); + +// Returns node information based on node names and versions +nodeTypesController.post( + '/', + ResponseHelper.send(async (req: express.Request): Promise => { + const nodeInfos = _.get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[]; + + const defaultLocale = config.getEnv('defaultLocale'); + + if (defaultLocale === 'en') { + return nodeInfos.reduce((acc, { name, version }) => { + const { description } = NodeTypes().getByNameAndVersion(name, version); + acc.push(injectCustomApiCallOption(description)); + return acc; + }, []); + } + + async function populateTranslation( + name: string, + version: number, + nodeTypes: INodeTypeDescription[], + ) { + const { description, sourcePath } = NodeTypes().getWithSourcePath(name, version); + const translationPath = await getNodeTranslationPath({ + nodeSourcePath: sourcePath, + longNodeType: description.name, + locale: defaultLocale, + }); + + try { + const translation = await readFile(translationPath, 'utf8'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + description.translation = JSON.parse(translation); + } catch (error) { + // ignore - no translation exists at path + } + + nodeTypes.push(injectCustomApiCallOption(description)); + } + + const nodeTypes: INodeTypeDescription[] = []; + + const promises = nodeInfos.map(async ({ name, version }) => + populateTranslation(name, version, nodeTypes), + ); + + await Promise.all(promises); + + return nodeTypes; + }), +);