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:
कारतोफ्फेलस्क्रिप्ट™ 2023-11-17 12:03:05 +01:00 committed by GitHub
parent 05ed86c64b
commit fc60e9a809
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 426 additions and 572 deletions

View file

@ -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');

View file

@ -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
// ----------------------------------------

View 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,
);
}
}

View file

@ -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

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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)!;
}
}

View file

@ -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);
}
}

View file

@ -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[];
}
}

View file

@ -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 };

View file

@ -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;

View file

@ -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,
);
}

View file

@ -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 } : {}),

View file

@ -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 {