mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Convert dynamic node-parameter routes to a decorated controller (no-changelog) (#7284)
1. Reduce a lot of code duplication 2. Move more endpoints out of `Server.ts` 3. Move all query-param parsing and validation into a middleware to make the route handlers simpler.
This commit is contained in:
parent
05ed86c64b
commit
fc60e9a809
|
@ -361,7 +361,7 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
it('should not retrieve remote options when a parameter value changes', () => {
|
||||
cy.intercept('/rest/node-parameter-options?**', cy.spy().as('fetchParameterOptions'));
|
||||
cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions'));
|
||||
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
|
||||
// Type something into the field
|
||||
ndv.actions.typeIntoParameterInput('otherField', 'test');
|
||||
|
|
|
@ -19,23 +19,12 @@ import type { ServeStaticOptions } from 'serve-static';
|
|||
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
|
||||
import { Not, In } from 'typeorm';
|
||||
|
||||
import {
|
||||
LoadMappingOptions,
|
||||
LoadNodeParameterOptions,
|
||||
LoadNodeListSearch,
|
||||
InstanceSettings,
|
||||
} from 'n8n-core';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type {
|
||||
INodeCredentials,
|
||||
INodeListSearchResult,
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
INodeTypeNameVersion,
|
||||
ICredentialTypes,
|
||||
ExecutionStatus,
|
||||
IExecutionsSummary,
|
||||
ResourceMapperFields,
|
||||
IN8nUISettings,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
|
@ -57,17 +46,11 @@ import {
|
|||
TEMPLATES_DIR,
|
||||
} from '@/constants';
|
||||
import { credentialsController } from '@/credentials/credentials.controller';
|
||||
import type {
|
||||
CurlHelper,
|
||||
ExecutionRequest,
|
||||
NodeListSearchRequest,
|
||||
NodeParameterOptionsRequest,
|
||||
ResourceMapperRequest,
|
||||
WorkflowRequest,
|
||||
} from '@/requests';
|
||||
import type { CurlHelper, ExecutionRequest, WorkflowRequest } from '@/requests';
|
||||
import { registerController } from '@/decorators';
|
||||
import { AuthController } from '@/controllers/auth.controller';
|
||||
import { BinaryDataController } from '@/controllers/binaryData.controller';
|
||||
import { DynamicNodeParametersController } from '@/controllers/dynamicNodeParameters.controller';
|
||||
import { LdapController } from '@/controllers/ldap.controller';
|
||||
import { MeController } from '@/controllers/me.controller';
|
||||
import { MFAController } from '@/controllers/mfa.controller';
|
||||
|
@ -93,7 +76,6 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
|||
import { NodeTypes } from '@/NodeTypes';
|
||||
import * as ResponseHelper from '@/ResponseHelper';
|
||||
import { WaitTracker } from '@/WaitTracker';
|
||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
||||
import { EventBusController } from '@/eventbus/eventBus.controller';
|
||||
import { EventBusControllerEE } from '@/eventbus/eventBus.controller.ee';
|
||||
|
@ -277,6 +259,7 @@ export class Server extends AbstractServer {
|
|||
postHog,
|
||||
),
|
||||
Container.get(MeController),
|
||||
Container.get(DynamicNodeParametersController),
|
||||
new NodeTypesController(config, nodeTypes),
|
||||
Container.get(PasswordResetController),
|
||||
Container.get(TagsController),
|
||||
|
@ -450,170 +433,6 @@ export class Server extends AbstractServer {
|
|||
this.logger.warn(`Source Control initialization failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
|
||||
// Returns parameter values which normally get loaded from an external API or
|
||||
// get generated dynamically
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/node-parameter-options`,
|
||||
ResponseHelper.send(
|
||||
async (req: NodeParameterOptionsRequest): Promise<INodePropertyOptions[]> => {
|
||||
const nodeTypeAndVersion = jsonParse(
|
||||
req.query.nodeTypeAndVersion,
|
||||
) as INodeTypeNameVersion;
|
||||
|
||||
const { path, methodName } = req.query;
|
||||
|
||||
const currentNodeParameters = jsonParse(
|
||||
req.query.currentNodeParameters,
|
||||
) as INodeParameters;
|
||||
|
||||
let credentials: INodeCredentials | undefined;
|
||||
|
||||
if (req.query.credentials) {
|
||||
credentials = jsonParse(req.query.credentials);
|
||||
}
|
||||
|
||||
const loadDataInstance = new LoadNodeParameterOptions(
|
||||
nodeTypeAndVersion,
|
||||
this.nodeTypes,
|
||||
path,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
);
|
||||
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||
req.user.id,
|
||||
currentNodeParameters,
|
||||
);
|
||||
|
||||
if (methodName) {
|
||||
return loadDataInstance.getOptionsViaMethodName(methodName, additionalData);
|
||||
}
|
||||
// @ts-ignore
|
||||
if (req.query.loadOptions) {
|
||||
return loadDataInstance.getOptionsViaRequestProperty(
|
||||
// @ts-ignore
|
||||
jsonParse(req.query.loadOptions as string),
|
||||
additionalData,
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Returns parameter values which normally get loaded from an external API or
|
||||
// get generated dynamically
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/nodes-list-search`,
|
||||
ResponseHelper.send(
|
||||
async (
|
||||
req: NodeListSearchRequest,
|
||||
res: express.Response,
|
||||
): Promise<INodeListSearchResult | undefined> => {
|
||||
const nodeTypeAndVersion = jsonParse(
|
||||
req.query.nodeTypeAndVersion,
|
||||
) as INodeTypeNameVersion;
|
||||
|
||||
const { path, methodName } = req.query;
|
||||
|
||||
if (!req.query.currentNodeParameters) {
|
||||
throw new ResponseHelper.BadRequestError(
|
||||
'Parameter currentNodeParameters is required.',
|
||||
);
|
||||
}
|
||||
|
||||
const currentNodeParameters = jsonParse(
|
||||
req.query.currentNodeParameters,
|
||||
) as INodeParameters;
|
||||
|
||||
let credentials: INodeCredentials | undefined;
|
||||
|
||||
if (req.query.credentials) {
|
||||
credentials = jsonParse(req.query.credentials);
|
||||
}
|
||||
|
||||
const listSearchInstance = new LoadNodeListSearch(
|
||||
nodeTypeAndVersion,
|
||||
this.nodeTypes,
|
||||
path,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
);
|
||||
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||
req.user.id,
|
||||
currentNodeParameters,
|
||||
);
|
||||
|
||||
if (methodName) {
|
||||
return listSearchInstance.getOptionsViaMethodName(
|
||||
methodName,
|
||||
additionalData,
|
||||
req.query.filter,
|
||||
req.query.paginationToken,
|
||||
);
|
||||
}
|
||||
|
||||
throw new ResponseHelper.BadRequestError('Parameter methodName is required.');
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/get-mapping-fields`,
|
||||
ResponseHelper.send(
|
||||
async (
|
||||
req: ResourceMapperRequest,
|
||||
res: express.Response,
|
||||
): Promise<ResourceMapperFields | undefined> => {
|
||||
const nodeTypeAndVersion = jsonParse(
|
||||
req.query.nodeTypeAndVersion,
|
||||
) as INodeTypeNameVersion;
|
||||
|
||||
const { path, methodName } = req.query;
|
||||
|
||||
if (!req.query.currentNodeParameters) {
|
||||
throw new ResponseHelper.BadRequestError(
|
||||
'Parameter currentNodeParameters is required.',
|
||||
);
|
||||
}
|
||||
|
||||
const currentNodeParameters = jsonParse(
|
||||
req.query.currentNodeParameters,
|
||||
) as INodeParameters;
|
||||
|
||||
let credentials: INodeCredentials | undefined;
|
||||
|
||||
if (req.query.credentials) {
|
||||
credentials = jsonParse(req.query.credentials);
|
||||
}
|
||||
|
||||
const loadMappingOptionsInstance = new LoadMappingOptions(
|
||||
nodeTypeAndVersion,
|
||||
this.nodeTypes,
|
||||
path,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
);
|
||||
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(
|
||||
req.user.id,
|
||||
currentNodeParameters,
|
||||
);
|
||||
|
||||
const fields = await loadMappingOptionsInstance.getOptionsViaMethodName(
|
||||
methodName,
|
||||
additionalData,
|
||||
);
|
||||
|
||||
return fields;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// Active Workflows
|
||||
// ----------------------------------------
|
||||
|
|
120
packages/cli/src/controllers/dynamicNodeParameters.controller.ts
Normal file
120
packages/cli/src/controllers/dynamicNodeParameters.controller.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { Service } from 'typedi';
|
||||
import type { RequestHandler } from 'express';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import type {
|
||||
INodeListSearchResult,
|
||||
INodePropertyOptions,
|
||||
ResourceMapperFields,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
|
||||
import { Authorized, Get, Middleware, RestController } from '@/decorators';
|
||||
import { getBase } from '@/WorkflowExecuteAdditionalData';
|
||||
import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service';
|
||||
import { DynamicNodeParametersRequest } from '@/requests';
|
||||
import { BadRequestError } from '@/ResponseHelper';
|
||||
|
||||
const assertMethodName: RequestHandler = (req, res, next) => {
|
||||
const { methodName } = req.query as DynamicNodeParametersRequest.BaseRequest['query'];
|
||||
if (!methodName) {
|
||||
throw new BadRequestError('Parameter methodName is required.');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
@Service()
|
||||
@Authorized()
|
||||
@RestController('/dynamic-node-parameters')
|
||||
export class DynamicNodeParametersController {
|
||||
constructor(private readonly service: DynamicNodeParametersService) {}
|
||||
|
||||
@Middleware()
|
||||
parseQueryParams(
|
||||
req: DynamicNodeParametersRequest.BaseRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.query;
|
||||
if (!nodeTypeAndVersion) {
|
||||
throw new BadRequestError('Parameter nodeTypeAndVersion is required.');
|
||||
}
|
||||
if (!currentNodeParameters) {
|
||||
throw new BadRequestError('Parameter currentNodeParameters is required.');
|
||||
}
|
||||
|
||||
req.params = {
|
||||
nodeTypeAndVersion: jsonParse(nodeTypeAndVersion),
|
||||
currentNodeParameters: jsonParse(currentNodeParameters),
|
||||
credentials: credentials ? jsonParse(credentials) : undefined,
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/** Returns parameter values which normally get loaded from an external API or get generated dynamically */
|
||||
@Get('/options')
|
||||
async getOptions(req: DynamicNodeParametersRequest.Options): Promise<INodePropertyOptions[]> {
|
||||
const { path, methodName, loadOptions } = req.query;
|
||||
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.params;
|
||||
const additionalData = await getBase(req.user.id, currentNodeParameters);
|
||||
|
||||
if (methodName) {
|
||||
return this.service.getOptionsViaMethodName(
|
||||
methodName,
|
||||
path,
|
||||
additionalData,
|
||||
nodeTypeAndVersion,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
);
|
||||
}
|
||||
|
||||
if (loadOptions) {
|
||||
return this.service.getOptionsViaLoadOptions(
|
||||
jsonParse(loadOptions),
|
||||
additionalData,
|
||||
nodeTypeAndVersion,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@Get('/resource-locator-results', { middlewares: [assertMethodName] })
|
||||
async getResourceLocatorResults(
|
||||
req: DynamicNodeParametersRequest.ResourceLocatorResults,
|
||||
): Promise<INodeListSearchResult | undefined> {
|
||||
const { path, methodName, filter, paginationToken } = req.query;
|
||||
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.params;
|
||||
const additionalData = await getBase(req.user.id, currentNodeParameters);
|
||||
return this.service.getResourceLocatorResults(
|
||||
methodName,
|
||||
path,
|
||||
additionalData,
|
||||
nodeTypeAndVersion,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
filter,
|
||||
paginationToken,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/resource-mapper-fields', { middlewares: [assertMethodName] })
|
||||
async getResourceMappingFields(
|
||||
req: DynamicNodeParametersRequest.ResourceMapperFields,
|
||||
): Promise<ResourceMapperFields | undefined> {
|
||||
const { path, methodName } = req.query;
|
||||
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.params;
|
||||
const additionalData = await getBase(req.user.id, currentNodeParameters);
|
||||
return this.service.getResourceMappingFields(
|
||||
methodName,
|
||||
path,
|
||||
additionalData,
|
||||
nodeTypeAndVersion,
|
||||
currentNodeParameters,
|
||||
credentials,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ import type {
|
|||
IDataObject,
|
||||
INode,
|
||||
INodeCredentialTestRequest,
|
||||
INodeCredentials,
|
||||
INodeParameters,
|
||||
INodeTypeNameVersion,
|
||||
IPinData,
|
||||
IRunData,
|
||||
IUser,
|
||||
|
@ -403,57 +406,43 @@ export declare namespace OAuthRequest {
|
|||
}
|
||||
|
||||
// ----------------------------------
|
||||
// /node-parameter-options
|
||||
// /dynamic-node-parameters
|
||||
// ----------------------------------
|
||||
export declare namespace DynamicNodeParametersRequest {
|
||||
type BaseRequest<QueryParams = {}> = AuthenticatedRequest<
|
||||
{
|
||||
nodeTypeAndVersion: INodeTypeNameVersion;
|
||||
currentNodeParameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
path: string;
|
||||
nodeTypeAndVersion: string;
|
||||
currentNodeParameters: string;
|
||||
methodName?: string;
|
||||
credentials?: string;
|
||||
} & QueryParams
|
||||
>;
|
||||
|
||||
export type NodeParameterOptionsRequest = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
nodeTypeAndVersion: string;
|
||||
/** GET /dynamic-node-parameters/options */
|
||||
type Options = BaseRequest<{
|
||||
loadOptions?: string;
|
||||
}>;
|
||||
|
||||
/** GET /dynamic-node-parameters/resource-locator-results */
|
||||
type ResourceLocatorResults = BaseRequest<{
|
||||
methodName: string;
|
||||
path: string;
|
||||
currentNodeParameters: string;
|
||||
credentials: string;
|
||||
}
|
||||
>;
|
||||
|
||||
// ----------------------------------
|
||||
// /node-list-search
|
||||
// ----------------------------------
|
||||
|
||||
export type NodeListSearchRequest = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
nodeTypeAndVersion: string;
|
||||
methodName: string;
|
||||
path: string;
|
||||
currentNodeParameters: string;
|
||||
credentials: string;
|
||||
filter?: string;
|
||||
paginationToken?: string;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
|
||||
// ----------------------------------
|
||||
// /get-mapping-fields
|
||||
// ----------------------------------
|
||||
|
||||
export type ResourceMapperRequest = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
nodeTypeAndVersion: string;
|
||||
/** GET dynamic-node-parameters/resource-mapper-fields */
|
||||
type ResourceMapperFields = BaseRequest<{
|
||||
methodName: string;
|
||||
path: string;
|
||||
currentNodeParameters: string;
|
||||
credentials: string;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// /tags
|
||||
|
|
230
packages/cli/src/services/dynamicNodeParameters.service.ts
Normal file
230
packages/cli/src/services/dynamicNodeParameters.service.ts
Normal file
|
@ -0,0 +1,230 @@
|
|||
import { Service } from 'typedi';
|
||||
import type {
|
||||
ILoadOptions,
|
||||
ILoadOptionsFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodeListSearchResult,
|
||||
INodeProperties,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
IRunExecutionData,
|
||||
ITaskDataConnections,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
ResourceMapperFields,
|
||||
INodeCredentials,
|
||||
INodeParameters,
|
||||
INodeTypeNameVersion,
|
||||
} from 'n8n-workflow';
|
||||
import { Workflow, RoutingNode } from 'n8n-workflow';
|
||||
import { NodeExecuteFunctions } from 'n8n-core';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
|
||||
@Service()
|
||||
export class DynamicNodeParametersService {
|
||||
constructor(private nodeTypes: NodeTypes) {}
|
||||
|
||||
/** Returns the available options via a predefined method */
|
||||
async getOptionsViaMethodName(
|
||||
methodName: string,
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||
const method = this.getMethod('loadOptions', methodName, nodeType);
|
||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return method.call(thisArgs);
|
||||
}
|
||||
|
||||
/** Returns the available options via a loadOptions param */
|
||||
async getOptionsViaLoadOptions(
|
||||
loadOptions: ILoadOptions,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||
if (!nodeType.description.requestDefaults?.baseURL) {
|
||||
// This in in here for now for security reasons.
|
||||
// Background: As the full data for the request to make does get send, and the auth data
|
||||
// will then be applied, would it be possible to retrieve that data like that. By at least
|
||||
// requiring a baseURL to be defined can at least not a random server be called.
|
||||
// In the future this code has to get improved that it does not use the request information from
|
||||
// the request rather resolves it via the parameter-path and nodeType data.
|
||||
throw new Error(
|
||||
`The node-type "${nodeType.description.name}" does not exist or does not have "requestDefaults.baseURL" defined!`,
|
||||
);
|
||||
}
|
||||
|
||||
const mode = 'internal';
|
||||
const runIndex = 0;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
const runExecutionData: IRunExecutionData = { resultData: { runData: {} } };
|
||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||
const node = workflow.nodes[0];
|
||||
|
||||
const routingNode = new RoutingNode(
|
||||
workflow,
|
||||
node,
|
||||
connectionInputData,
|
||||
runExecutionData ?? null,
|
||||
additionalData,
|
||||
mode,
|
||||
);
|
||||
|
||||
// Create copy of node-type with the single property we want to get the data off
|
||||
const tempNode: INodeType = {
|
||||
...nodeType,
|
||||
...{
|
||||
description: {
|
||||
...nodeType.description,
|
||||
properties: [
|
||||
{
|
||||
displayName: '',
|
||||
type: 'string',
|
||||
name: '',
|
||||
default: '',
|
||||
routing: loadOptions.routing,
|
||||
} as INodeProperties,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inputData: ITaskDataConnections = {
|
||||
main: [[{ json: {} }]],
|
||||
};
|
||||
|
||||
const optionsData = await routingNode.runNode(
|
||||
inputData,
|
||||
runIndex,
|
||||
tempNode,
|
||||
{ node, source: null, data: {} },
|
||||
NodeExecuteFunctions,
|
||||
);
|
||||
|
||||
if (optionsData?.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(optionsData)) {
|
||||
throw new Error('The returned data is not an array!');
|
||||
}
|
||||
|
||||
return optionsData[0].map((item) => item.json) as unknown as INodePropertyOptions[];
|
||||
}
|
||||
|
||||
async getResourceLocatorResults(
|
||||
methodName: string,
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||
const method = this.getMethod('listSearch', methodName, nodeType);
|
||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return method.call(thisArgs, filter, paginationToken);
|
||||
}
|
||||
|
||||
/** Returns the available mapping fields for the ResourceMapper component */
|
||||
async getResourceMappingFields(
|
||||
methodName: string,
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
): Promise<ResourceMapperFields> {
|
||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||
const method = this.getMethod('resourceMapping', methodName, nodeType);
|
||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return method.call(thisArgs);
|
||||
}
|
||||
|
||||
private getMethod(
|
||||
type: 'resourceMapping',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
private getMethod(
|
||||
type: 'listSearch',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string | undefined,
|
||||
paginationToken?: string | undefined,
|
||||
) => Promise<INodeListSearchResult>;
|
||||
private getMethod(
|
||||
type: 'loadOptions',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
||||
|
||||
private getMethod(
|
||||
type: 'resourceMapping' | 'listSearch' | 'loadOptions',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
) {
|
||||
const method = nodeType.methods?.[type]?.[methodName];
|
||||
if (typeof method !== 'function') {
|
||||
throw new Error(
|
||||
`The node-type "${nodeType.description.name}" does not have the method "${methodName}" defined!`,
|
||||
);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private getNodeType({ name, version }: INodeTypeNameVersion) {
|
||||
return this.nodeTypes.getByNameAndVersion(name, version);
|
||||
}
|
||||
|
||||
private getWorkflow(
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
) {
|
||||
const node: INode = {
|
||||
parameters: currentNodeParameters,
|
||||
id: 'uuid-1234',
|
||||
name: 'Temp-Node',
|
||||
type: nodeTypeAndVersion.name,
|
||||
typeVersion: nodeTypeAndVersion.version,
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
if (credentials) {
|
||||
node.credentials = credentials;
|
||||
}
|
||||
|
||||
return new Workflow({
|
||||
nodes: [node],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes: this.nodeTypes,
|
||||
});
|
||||
}
|
||||
|
||||
private getThisArg(
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
workflow: Workflow,
|
||||
) {
|
||||
const node = Object.values(workflow.nodes)[0];
|
||||
return NodeExecuteFunctions.getLoadOptionsFunctions(workflow, node, path, additionalData);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import type { IWorkflowExecuteAdditionalData, ResourceMapperFields } from 'n8n-workflow';
|
||||
|
||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||
import { LoadNodeDetails } from './LoadNodeDetails';
|
||||
|
||||
export class LoadMappingOptions extends LoadNodeDetails {
|
||||
/**
|
||||
* Returns the available mapping fields for the ResourceMapper component
|
||||
*/
|
||||
async getOptionsViaMethodName(
|
||||
methodName: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
): Promise<ResourceMapperFields> {
|
||||
const node = this.getTempNode();
|
||||
|
||||
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const method = nodeType?.methods?.resourceMapping?.[methodName];
|
||||
|
||||
if (typeof method !== 'function') {
|
||||
throw new Error(
|
||||
`The node-type "${node.type}" does not have the method "${methodName}" defined!`,
|
||||
);
|
||||
}
|
||||
|
||||
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(
|
||||
this.workflow,
|
||||
node,
|
||||
this.path,
|
||||
additionalData,
|
||||
);
|
||||
|
||||
return method.call(thisArgs);
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import type { INode } from 'n8n-workflow';
|
||||
import {
|
||||
Workflow,
|
||||
INodeCredentials,
|
||||
INodeParameters,
|
||||
INodeTypeNameVersion,
|
||||
INodeTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const TEMP_NODE_NAME = 'Temp-Node';
|
||||
const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
|
||||
|
||||
export abstract class LoadNodeDetails {
|
||||
path: string;
|
||||
|
||||
workflow: Workflow;
|
||||
|
||||
constructor(
|
||||
nodeTypeNameAndVersion: INodeTypeNameVersion,
|
||||
nodeTypes: INodeTypes,
|
||||
path: string,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
) {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(
|
||||
nodeTypeNameAndVersion.name,
|
||||
nodeTypeNameAndVersion.version,
|
||||
);
|
||||
|
||||
this.path = path;
|
||||
|
||||
if (nodeType === undefined) {
|
||||
throw new Error(
|
||||
`The node-type "${nodeTypeNameAndVersion.name} v${nodeTypeNameAndVersion.version}" is not known!`,
|
||||
);
|
||||
}
|
||||
|
||||
const nodeData: INode = {
|
||||
parameters: currentNodeParameters,
|
||||
id: 'uuid-1234',
|
||||
name: TEMP_NODE_NAME,
|
||||
type: nodeTypeNameAndVersion.name,
|
||||
typeVersion: nodeTypeNameAndVersion.version,
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
if (credentials) {
|
||||
nodeData.credentials = credentials;
|
||||
}
|
||||
|
||||
const workflowData = {
|
||||
nodes: [nodeData],
|
||||
connections: {},
|
||||
};
|
||||
|
||||
this.workflow = new Workflow({
|
||||
nodes: workflowData.nodes,
|
||||
connections: workflowData.connections,
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data of a fake workflow
|
||||
*/
|
||||
getWorkflowData() {
|
||||
return {
|
||||
name: TEMP_WORKFLOW_NAME,
|
||||
active: false,
|
||||
connections: {},
|
||||
nodes: Object.values(this.workflow.nodes),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTempNode() {
|
||||
return this.workflow.getNode(TEMP_NODE_NAME)!;
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import type { INodeListSearchResult, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||
|
||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||
import { LoadNodeDetails } from './LoadNodeDetails';
|
||||
|
||||
export class LoadNodeListSearch extends LoadNodeDetails {
|
||||
/**
|
||||
* Returns the available options via a predefined method
|
||||
*/
|
||||
async getOptionsViaMethodName(
|
||||
methodName: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const node = this.getTempNode();
|
||||
|
||||
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const method = nodeType?.methods?.listSearch?.[methodName];
|
||||
|
||||
if (typeof method !== 'function') {
|
||||
throw new Error(
|
||||
`The node-type "${node.type}" does not have the method "${methodName}" defined!`,
|
||||
);
|
||||
}
|
||||
|
||||
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(
|
||||
this.workflow,
|
||||
node,
|
||||
this.path,
|
||||
additionalData,
|
||||
);
|
||||
|
||||
return method.call(thisArgs, filter, paginationToken);
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import type {
|
||||
ILoadOptions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
IRunExecutionData,
|
||||
ITaskDataConnections,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
} from 'n8n-workflow';
|
||||
import { RoutingNode } from 'n8n-workflow';
|
||||
|
||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||
import { LoadNodeDetails } from './LoadNodeDetails';
|
||||
|
||||
export class LoadNodeParameterOptions extends LoadNodeDetails {
|
||||
/**
|
||||
* Returns the available options via a predefined method
|
||||
*/
|
||||
async getOptionsViaMethodName(
|
||||
methodName: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const node = this.getTempNode();
|
||||
|
||||
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const method = nodeType?.methods?.loadOptions?.[methodName];
|
||||
|
||||
if (typeof method !== 'function') {
|
||||
throw new Error(
|
||||
`The node-type "${node.type}" does not have the method "${methodName}" defined!`,
|
||||
);
|
||||
}
|
||||
|
||||
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(
|
||||
this.workflow,
|
||||
node,
|
||||
this.path,
|
||||
additionalData,
|
||||
);
|
||||
|
||||
return method.call(thisArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available options via a load request information
|
||||
*/
|
||||
async getOptionsViaRequestProperty(
|
||||
loadOptions: ILoadOptions,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const node = this.getTempNode();
|
||||
|
||||
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node?.typeVersion);
|
||||
|
||||
if (!nodeType?.description?.requestDefaults?.baseURL) {
|
||||
// This in in here for now for security reasons.
|
||||
// Background: As the full data for the request to make does get send, and the auth data
|
||||
// will then be applied, would it be possible to retrieve that data like that. By at least
|
||||
// requiring a baseURL to be defined can at least not a random server be called.
|
||||
// In the future this code has to get improved that it does not use the request information from
|
||||
// the request rather resolves it via the parameter-path and nodeType data.
|
||||
throw new Error(
|
||||
`The node-type "${node.type}" does not exist or does not have "requestDefaults.baseURL" defined!`,
|
||||
);
|
||||
}
|
||||
|
||||
const mode = 'internal';
|
||||
const runIndex = 0;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
const runExecutionData: IRunExecutionData = { resultData: { runData: {} } };
|
||||
|
||||
const routingNode = new RoutingNode(
|
||||
this.workflow,
|
||||
node,
|
||||
connectionInputData,
|
||||
runExecutionData ?? null,
|
||||
additionalData,
|
||||
mode,
|
||||
);
|
||||
|
||||
// Create copy of node-type with the single property we want to get the data off
|
||||
const tempNode: INodeType = {
|
||||
...nodeType,
|
||||
...{
|
||||
description: {
|
||||
...nodeType.description,
|
||||
properties: [
|
||||
{
|
||||
displayName: '',
|
||||
type: 'string',
|
||||
name: '',
|
||||
default: '',
|
||||
routing: loadOptions.routing,
|
||||
} as INodeProperties,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inputData: ITaskDataConnections = {
|
||||
main: [[{ json: {} }]],
|
||||
};
|
||||
|
||||
const optionsData = await routingNode.runNode(
|
||||
inputData,
|
||||
runIndex,
|
||||
tempNode,
|
||||
{ node, source: null, data: {} },
|
||||
NodeExecuteFunctions,
|
||||
);
|
||||
|
||||
if (optionsData?.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(optionsData)) {
|
||||
throw new Error('The returned data is not an array!');
|
||||
}
|
||||
|
||||
return optionsData[0].map((item) => item.json) as unknown as INodePropertyOptions[];
|
||||
}
|
||||
}
|
|
@ -10,9 +10,6 @@ export * from './Credentials';
|
|||
export * from './DirectoryLoader';
|
||||
export * from './Interfaces';
|
||||
export { InstanceSettings } from './InstanceSettings';
|
||||
export * from './LoadMappingOptions';
|
||||
export * from './LoadNodeParameterOptions';
|
||||
export * from './LoadNodeListSearch';
|
||||
export * from './NodeExecuteFunctions';
|
||||
export * from './WorkflowExecute';
|
||||
export { NodeExecuteFunctions };
|
||||
|
|
|
@ -1355,17 +1355,6 @@ export interface ITabBarItem {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IResourceLocatorReqParams {
|
||||
nodeTypeAndVersion: INodeTypeNameVersion;
|
||||
path: string;
|
||||
methodName?: string;
|
||||
searchList?: ILoadOptions;
|
||||
currentNodeParameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
filter?: string;
|
||||
paginationToken?: unknown;
|
||||
}
|
||||
|
||||
export interface IResourceLocatorResultExpanded extends INodeListSearchItems {
|
||||
linkAlt?: string;
|
||||
}
|
||||
|
@ -1473,13 +1462,30 @@ export type NodeAuthenticationOption = {
|
|||
displayOptions?: IDisplayOptions;
|
||||
};
|
||||
|
||||
export interface ResourceMapperReqParams {
|
||||
nodeTypeAndVersion: INodeTypeNameVersion;
|
||||
path: string;
|
||||
methodName?: string;
|
||||
currentNodeParameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
export declare namespace DynamicNodeParameters {
|
||||
interface BaseRequest {
|
||||
path: string;
|
||||
nodeTypeAndVersion: INodeTypeNameVersion;
|
||||
currentNodeParameters: INodeParameters;
|
||||
methodName?: string;
|
||||
credentials?: INodeCredentials;
|
||||
}
|
||||
|
||||
interface OptionsRequest extends BaseRequest {
|
||||
loadOptions?: ILoadOptions;
|
||||
}
|
||||
|
||||
interface ResourceLocatorResultsRequest extends BaseRequest {
|
||||
methodName: string;
|
||||
filter?: string;
|
||||
paginationToken?: string;
|
||||
}
|
||||
|
||||
interface ResourceMapperFieldsRequest extends BaseRequest {
|
||||
methodName: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnvironmentVariable {
|
||||
id: number;
|
||||
key: string;
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
import type { DynamicNodeParameters, INodeTranslationHeaders, IRestApiContext } from '@/Interface';
|
||||
import type {
|
||||
INodeTranslationHeaders,
|
||||
IResourceLocatorReqParams,
|
||||
IRestApiContext,
|
||||
ResourceMapperReqParams,
|
||||
} from '@/Interface';
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptions,
|
||||
INodeCredentials,
|
||||
INodeListSearchResult,
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeTypeNameVersion,
|
||||
|
@ -38,38 +29,31 @@ export async function getNodesInformation(
|
|||
|
||||
export async function getNodeParameterOptions(
|
||||
context: IRestApiContext,
|
||||
sendData: {
|
||||
nodeTypeAndVersion: INodeTypeNameVersion;
|
||||
path: string;
|
||||
methodName?: string;
|
||||
loadOptions?: ILoadOptions;
|
||||
currentNodeParameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
},
|
||||
sendData: DynamicNodeParameters.OptionsRequest,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
return makeRestApiRequest(context, 'GET', '/node-parameter-options', sendData);
|
||||
return makeRestApiRequest(context, 'GET', '/dynamic-node-parameters/options', sendData);
|
||||
}
|
||||
|
||||
export async function getResourceLocatorResults(
|
||||
context: IRestApiContext,
|
||||
sendData: IResourceLocatorReqParams,
|
||||
sendData: DynamicNodeParameters.ResourceLocatorResultsRequest,
|
||||
): Promise<INodeListSearchResult> {
|
||||
return makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
'/nodes-list-search',
|
||||
sendData as unknown as IDataObject,
|
||||
'/dynamic-node-parameters/resource-locator-results',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getResourceMapperFields(
|
||||
context: IRestApiContext,
|
||||
sendData: ResourceMapperReqParams,
|
||||
sendData: DynamicNodeParameters.ResourceMapperFieldsRequest,
|
||||
): Promise<ResourceMapperFields> {
|
||||
return makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
'/get-mapping-fields',
|
||||
sendData as unknown as IDataObject,
|
||||
'/dynamic-node-parameters/resource-mapper-fields',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -166,7 +166,6 @@ import stringify from 'fast-json-stable-stringify';
|
|||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import type {
|
||||
ILoadOptions,
|
||||
INode,
|
||||
INodeCredentials,
|
||||
INodeListSearchItems,
|
||||
|
@ -688,9 +687,6 @@ export default defineComponent({
|
|||
const loadOptionsMethod = this.getPropertyArgument(this.currentMode, 'searchListMethod') as
|
||||
| string
|
||||
| undefined;
|
||||
const searchList = this.getPropertyArgument(this.currentMode, 'searchList') as
|
||||
| ILoadOptions
|
||||
| undefined;
|
||||
|
||||
const requestParams: IResourceLocatorReqParams = {
|
||||
nodeTypeAndVersion: {
|
||||
|
@ -699,7 +695,6 @@ export default defineComponent({
|
|||
},
|
||||
path: this.path,
|
||||
methodName: loadOptionsMethod,
|
||||
searchList,
|
||||
currentNodeParameters: resolvedNodeParameters,
|
||||
credentials: this.node.credentials,
|
||||
...(params.filter ? { filter: params.filter } : {}),
|
||||
|
|
|
@ -12,21 +12,14 @@ import {
|
|||
STORES,
|
||||
CREDENTIAL_ONLY_HTTP_NODE_VERSION,
|
||||
} from '@/constants';
|
||||
import type {
|
||||
INodeTypesState,
|
||||
IResourceLocatorReqParams,
|
||||
ResourceMapperReqParams,
|
||||
} from '@/Interface';
|
||||
import type { INodeTypesState, DynamicNodeParameters } from '@/Interface';
|
||||
import { addHeaders, addNodeTranslation } from '@/plugins/i18n';
|
||||
import { omit } from '@/utils';
|
||||
import type {
|
||||
ConnectionTypes,
|
||||
ILoadOptions,
|
||||
INode,
|
||||
INodeCredentials,
|
||||
INodeListSearchResult,
|
||||
INodeOutputConfiguration,
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeTypeNameVersion,
|
||||
|
@ -273,25 +266,20 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
|||
addHeaders(headers, rootStore.defaultLocale);
|
||||
}
|
||||
},
|
||||
async getNodeParameterOptions(sendData: {
|
||||
nodeTypeAndVersion: INodeTypeNameVersion;
|
||||
path: string;
|
||||
methodName?: string;
|
||||
loadOptions?: ILoadOptions;
|
||||
currentNodeParameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
}): Promise<INodePropertyOptions[]> {
|
||||
async getNodeParameterOptions(
|
||||
sendData: DynamicNodeParameters.OptionsRequest,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const rootStore = useRootStore();
|
||||
return getNodeParameterOptions(rootStore.getRestApiContext, sendData);
|
||||
},
|
||||
async getResourceLocatorResults(
|
||||
sendData: IResourceLocatorReqParams,
|
||||
sendData: DynamicNodeParameters.ResourceLocatorResultsRequest,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const rootStore = useRootStore();
|
||||
return getResourceLocatorResults(rootStore.getRestApiContext, sendData);
|
||||
},
|
||||
async getResourceMapperFields(
|
||||
sendData: ResourceMapperReqParams,
|
||||
sendData: DynamicNodeParameters.ResourceMapperFieldsRequest,
|
||||
): Promise<ResourceMapperFields | null> {
|
||||
const rootStore = useRootStore();
|
||||
try {
|
||||
|
|
Loading…
Reference in a new issue