first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const auditOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'get',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['audit'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Generate',
|
||||
value: 'generate',
|
||||
action: 'Generate a security audit',
|
||||
description: 'Generate a security audit for this n8n instance',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/audit',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const auditFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Additional Options',
|
||||
name: 'additionalOptions',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['audit'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
body: {
|
||||
additionalOptions: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Categories',
|
||||
name: 'categories',
|
||||
description: 'Risk categories to include in the audit',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
options: [
|
||||
{
|
||||
name: 'Credentials',
|
||||
value: 'credentials',
|
||||
},
|
||||
{
|
||||
name: 'Database',
|
||||
value: 'database',
|
||||
},
|
||||
{
|
||||
name: 'Filesystem',
|
||||
value: 'filesystem',
|
||||
},
|
||||
{
|
||||
name: 'Instance',
|
||||
value: 'instance',
|
||||
},
|
||||
{
|
||||
name: 'Nodes',
|
||||
value: 'nodes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Days Abandoned Workflow',
|
||||
name: 'daysAbandonedWorkflow',
|
||||
description: 'Days for a workflow to be considered abandoned if not executed',
|
||||
type: 'number',
|
||||
default: 90,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,169 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { parseAndSetBodyJson } from './GenericFunctions';
|
||||
|
||||
export const credentialOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'create',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['credential'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
action: 'Create a credential',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/credentials',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
action: 'Delete a credential',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/credentials/{{ $parameter.credentialId }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Schema',
|
||||
value: 'getSchema',
|
||||
action: 'Get credential data schema for type',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/credentials/schema/{{ $parameter.credentialTypeName }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const createOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. n8n account',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['credential'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
body: {
|
||||
name: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
description: 'Name of the new credential',
|
||||
},
|
||||
{
|
||||
displayName: 'Credential Type',
|
||||
name: 'credentialTypeName',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. n8nApi',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['credential'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
body: {
|
||||
type: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
description:
|
||||
"The available types depend on nodes installed on the n8n instance. Some built-in types include e.g. 'githubApi', 'notionApi', and 'slackApi'.",
|
||||
},
|
||||
{
|
||||
displayName: 'Data',
|
||||
name: 'data',
|
||||
type: 'json',
|
||||
default: '',
|
||||
placeholder:
|
||||
'// e.g. for n8nApi \n{\n "apiKey": "my-n8n-api-key",\n "baseUrl": "https://<name>.app.n8n.cloud/api/v1",\n}',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['credential'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
// Validate that the 'data' property is parseable as JSON and
|
||||
// set it into the request as body.data.
|
||||
preSend: [parseAndSetBodyJson('data', 'data')],
|
||||
},
|
||||
},
|
||||
description:
|
||||
"A valid JSON object with properties required for this Credential Type. To see the expected format, you can use 'Get Schema' operation.",
|
||||
},
|
||||
];
|
||||
|
||||
const deleteOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Credential ID',
|
||||
name: 'credentialId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['credential'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getSchemaOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Credential Type',
|
||||
name: 'credentialTypeName',
|
||||
default: '',
|
||||
placeholder: 'e.g. n8nApi',
|
||||
required: true,
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['credential'],
|
||||
operation: ['getSchema'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
"The available types depend on nodes installed on the n8n instance. Some built-in types include e.g. 'githubApi', 'notionApi', and 'slackApi'.",
|
||||
},
|
||||
];
|
||||
|
||||
export const credentialFields: INodeProperties[] = [
|
||||
...createOperation,
|
||||
...deleteOperation,
|
||||
...getSchemaOperation,
|
||||
];
|
||||
@@ -0,0 +1,252 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { getCursorPaginator } from './GenericFunctions';
|
||||
import { workflowIdLocator } from './WorkflowLocator';
|
||||
|
||||
export const executionOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'getAll',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
action: 'Get an execution',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/executions/{{ $parameter.executionId }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
action: 'Get many executions',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/executions',
|
||||
},
|
||||
send: {
|
||||
paginate: true,
|
||||
},
|
||||
operations: {
|
||||
pagination: getCursorPaginator(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
action: 'Delete an execution',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/executions/{{ $parameter.executionId }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Execution ID',
|
||||
name: 'executionId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 100,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 250,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
limit: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// Use the common workflowIdLocator, but provide a custom routing
|
||||
...workflowIdLocator,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'workflowId',
|
||||
value: '={{ $value || undefined }}',
|
||||
},
|
||||
},
|
||||
description: 'Workflow to filter the executions by',
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Error',
|
||||
value: 'error',
|
||||
},
|
||||
{
|
||||
name: 'Success',
|
||||
value: 'success',
|
||||
},
|
||||
{
|
||||
name: 'Waiting',
|
||||
value: 'waiting',
|
||||
},
|
||||
],
|
||||
default: 'success',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'status',
|
||||
value: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
description: 'Status to filter the executions by',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
placeholder: 'Add option',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Execution Details',
|
||||
name: 'activeWorkflows',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'includeData',
|
||||
value: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
description: 'Whether to include the detailed execution data',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Execution ID',
|
||||
name: 'executionId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
placeholder: 'Add option',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['execution'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Execution Details',
|
||||
name: 'activeWorkflows',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'includeData',
|
||||
value: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
description: 'Whether to include the detailed execution data',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const executionFields: INodeProperties[] = [
|
||||
...deleteOperation,
|
||||
...getAllOperation,
|
||||
...getOperation,
|
||||
];
|
||||
@@ -0,0 +1,236 @@
|
||||
import type {
|
||||
DeclarativeRestApiSettings,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecutePaginationFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
IRequestOptions,
|
||||
JsonObject,
|
||||
PreSendAction,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* A custom API request function to be used with the resourceLocator lookup queries.
|
||||
*/
|
||||
export async function apiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: object,
|
||||
query?: IDataObject,
|
||||
): Promise<any> {
|
||||
query = query || {};
|
||||
|
||||
type N8nApiCredentials = {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const credentials = await this.getCredentials<N8nApiCredentials>('n8nApi');
|
||||
const baseUrl = credentials.baseUrl;
|
||||
|
||||
const options: IRequestOptions = {
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
uri: `${baseUrl.replace(new RegExp('/$'), '')}/${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.requestWithAuthentication.call(this, 'n8nApi', options);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequestAllItems(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: object,
|
||||
query?: IDataObject,
|
||||
): Promise<any> {
|
||||
query = query || {};
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let nextCursor: string | undefined = undefined;
|
||||
let responseData;
|
||||
|
||||
do {
|
||||
query.cursor = nextCursor;
|
||||
query.limit = 100;
|
||||
responseData = await apiRequest.call(this, method, endpoint, body, query);
|
||||
returnData.push.apply(returnData, responseData.data as IDataObject[]);
|
||||
nextCursor = responseData.nextCursor as string | undefined;
|
||||
} while (nextCursor);
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cursor-based paginator to use with n8n 'getAll' type endpoints.
|
||||
*
|
||||
* It will look up a 'nextCursor' in the response and if the node has
|
||||
* 'returnAll' set to true, will consecutively include it as the 'cursor' query
|
||||
* parameter for the next request, effectively getting everything in slices.
|
||||
*
|
||||
* Prequisites:
|
||||
* - routing.send.paginate must be set to true, for all requests to go through here
|
||||
* - node is expected to have a boolean parameter 'returnAll'
|
||||
* - no postReceive action setting the rootProperty, to get the items mapped
|
||||
*
|
||||
* @returns A ready-to-use cursor-based paginator function.
|
||||
*/
|
||||
export const getCursorPaginator = () => {
|
||||
return async function cursorPagination(
|
||||
this: IExecutePaginationFunctions,
|
||||
requestOptions: DeclarativeRestApiSettings.ResultOptions,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
if (!requestOptions.options.qs) {
|
||||
requestOptions.options.qs = {};
|
||||
}
|
||||
|
||||
let executions: INodeExecutionData[] = [];
|
||||
let responseData: INodeExecutionData[];
|
||||
let nextCursor: string | undefined = undefined;
|
||||
const returnAll = this.getNodeParameter('returnAll', true) as boolean;
|
||||
|
||||
const extractItems = (page: INodeExecutionData) => {
|
||||
const items = page.json.data as IDataObject[];
|
||||
if (items) {
|
||||
// Extract the items themselves
|
||||
executions = executions.concat(items.map((item) => ({ json: item })));
|
||||
}
|
||||
};
|
||||
|
||||
do {
|
||||
requestOptions.options.qs.cursor = nextCursor;
|
||||
responseData = await this.makeRoutingRequest(requestOptions);
|
||||
|
||||
// Check for another page of items
|
||||
const lastItem = responseData[responseData.length - 1].json;
|
||||
nextCursor = lastItem.nextCursor as string | undefined;
|
||||
|
||||
responseData.forEach(extractItems);
|
||||
|
||||
// If we don't return all, just return the first page
|
||||
} while (returnAll && nextCursor);
|
||||
|
||||
return executions;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function to parse a node parameter as JSON and set it in the request body.
|
||||
* Throws a NodeOperationError is the content is not valid JSON or it cannot be set.
|
||||
*
|
||||
* Currently, parameters with type 'json' are not validated automatically.
|
||||
* Also mapping the value for 'body.data' declaratively has it treated as a string,
|
||||
* but some operations (e.g. POST /credentials) don't work unless it is set as an object.
|
||||
* To get the JSON-body operations to work consistently, we need to parse and set the body
|
||||
* manually.
|
||||
*
|
||||
* @param parameterName The name of the node parameter to parse
|
||||
* @param setAsBodyProperty An optional property name to set the parsed data into
|
||||
* @returns The requestOptions with its body replaced with the contents of the parameter
|
||||
*/
|
||||
export const parseAndSetBodyJson = (
|
||||
parameterName: string,
|
||||
setAsBodyProperty?: string,
|
||||
): PreSendAction => {
|
||||
return async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
try {
|
||||
const rawData = this.getNodeParameter(parameterName, '{}') as string;
|
||||
const parsedObject = JSON.parse(rawData);
|
||||
|
||||
// Set the parsed object to either as the request body directly, or as its sub-property
|
||||
if (setAsBodyProperty === undefined) {
|
||||
requestOptions.body = parsedObject;
|
||||
} else {
|
||||
requestOptions.body = Object.assign({}, requestOptions.body, {
|
||||
[setAsBodyProperty]: parsedObject,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
new Error(`The '${parameterName}' property must be valid JSON, but cannot be parsed`, {
|
||||
cause: err,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return requestOptions;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function to prepare the workflow object data for creation. It only sets
|
||||
* known workflow properties, for pre-emptively avoiding a HTTP 400 Bad Request
|
||||
* response until we have a better client-side schema validation mechanism.
|
||||
*
|
||||
* NOTE! This expects the requestOptions.body to already be set as an object,
|
||||
* so take care to first call parseAndSetBodyJson().
|
||||
*/
|
||||
export const prepareWorkflowCreateBody: PreSendAction = async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = requestOptions.body as IDataObject;
|
||||
const newBody: IDataObject = {};
|
||||
|
||||
newBody.name = body.name || 'My workflow';
|
||||
newBody.nodes = body.nodes || [];
|
||||
newBody.settings = body.settings || {};
|
||||
newBody.connections = body.connections || {};
|
||||
newBody.staticData = body.staticData || null;
|
||||
|
||||
requestOptions.body = newBody;
|
||||
|
||||
return requestOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function to prepare the workflow object data for update.
|
||||
*
|
||||
* NOTE! This expects the requestOptions.body to already be set as an object,
|
||||
* so take care to first call parseAndSetBodyJson().
|
||||
*/
|
||||
export const prepareWorkflowUpdateBody: PreSendAction = async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = requestOptions.body as IDataObject;
|
||||
const newBody: IDataObject = {};
|
||||
|
||||
if (body.name) {
|
||||
newBody.name = body.name;
|
||||
}
|
||||
if (body.nodes) {
|
||||
newBody.nodes = body.nodes;
|
||||
}
|
||||
if (body.settings) {
|
||||
newBody.settings = body.settings;
|
||||
}
|
||||
if (body.connections) {
|
||||
newBody.connections = body.connections;
|
||||
}
|
||||
if (body.staticData) {
|
||||
newBody.staticData = body.staticData;
|
||||
}
|
||||
|
||||
requestOptions.body = newBody;
|
||||
|
||||
return requestOptions;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.n8n",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Development", "Core Nodes"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/api/authentication/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.n8n/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["Workflow", "Execution"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Helpers", "Other Trigger Nodes"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NodeConnectionTypes, type INodeType, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import { auditFields, auditOperations } from './AuditDescription';
|
||||
import { credentialFields, credentialOperations } from './CredentialDescription';
|
||||
import { executionFields, executionOperations } from './ExecutionDescription';
|
||||
import { workflowFields, workflowOperations } from './WorkflowDescription';
|
||||
import { searchWorkflows } from './WorkflowLocator';
|
||||
|
||||
/**
|
||||
* The n8n node provides access to the n8n API.
|
||||
*
|
||||
* See: https://docs.n8n.io/api/api-reference/
|
||||
*/
|
||||
export class N8n implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'n8n',
|
||||
name: 'n8n',
|
||||
icon: 'file:n8n.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Handle events and perform actions on your n8n instance',
|
||||
defaults: {
|
||||
name: 'n8n',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'n8nApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
returnFullResponse: true,
|
||||
baseURL: '={{ $credentials.baseUrl.replace(new RegExp("/$"), "") }}',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Audit',
|
||||
value: 'audit',
|
||||
},
|
||||
{
|
||||
name: 'Credential',
|
||||
value: 'credential',
|
||||
},
|
||||
{
|
||||
name: 'Execution',
|
||||
value: 'execution',
|
||||
},
|
||||
{
|
||||
name: 'Workflow',
|
||||
value: 'workflow',
|
||||
},
|
||||
],
|
||||
default: 'workflow',
|
||||
},
|
||||
|
||||
...auditOperations,
|
||||
...auditFields,
|
||||
|
||||
...credentialOperations,
|
||||
...credentialFields,
|
||||
|
||||
...executionOperations,
|
||||
...executionFields,
|
||||
|
||||
...workflowOperations,
|
||||
...workflowFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch: {
|
||||
// Provide workflows search capability for the workflow resourceLocator
|
||||
searchWorkflows,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getCursorPaginator,
|
||||
parseAndSetBodyJson,
|
||||
prepareWorkflowCreateBody,
|
||||
prepareWorkflowUpdateBody,
|
||||
} from './GenericFunctions';
|
||||
import { workflowIdLocator } from './WorkflowLocator';
|
||||
|
||||
export const workflowOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'getAll',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Publish',
|
||||
value: 'activate',
|
||||
action: 'Publish a workflow',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
action: 'Create a workflow',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/workflows',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Unpublish',
|
||||
value: 'deactivate',
|
||||
action: 'Unpublish a workflow',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
action: 'Delete a workflow',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
action: 'Get a workflow',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
action: 'Get many workflows',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/workflows',
|
||||
},
|
||||
send: {
|
||||
paginate: true,
|
||||
},
|
||||
operations: {
|
||||
pagination: getCursorPaginator(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Version',
|
||||
value: 'getVersion',
|
||||
action: 'Get a workflow version',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
action: 'Update a workflow',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const activateOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['activate'],
|
||||
},
|
||||
},
|
||||
// The routing for resourceLocator-enabled properties currently needs to
|
||||
// happen in the property block where the property itself is defined, or
|
||||
// extractValue won't work when used with $parameter in routing.request.url.
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/workflows/{{ $value }}/activate',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['activate'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Version ID',
|
||||
name: 'versionId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'versionId',
|
||||
},
|
||||
},
|
||||
description: 'The version ID of the workflow to publish',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
description: 'Published version name (will be overwritten)',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 4,
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'description',
|
||||
},
|
||||
},
|
||||
description: 'Published version description (will be overwritten)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const createOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Workflow Object',
|
||||
name: 'workflowObject',
|
||||
type: 'json',
|
||||
default: '{ "name": "My workflow", "nodes": [], "connections": {}, "settings": {} }',
|
||||
placeholder:
|
||||
'{\n "name": "My workflow",\n "nodes": [],\n "connections": {},\n "settings": {}\n}',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [parseAndSetBodyJson('workflowObject'), prepareWorkflowCreateBody],
|
||||
},
|
||||
},
|
||||
description:
|
||||
"A valid JSON object with required fields: 'name', 'nodes', 'connections' and 'settings'. More information can be found in the <a href=\"https://docs.n8n.io/api/api-reference/#tag/workflow/paths/~1workflows/post\">documentation</a>.",
|
||||
},
|
||||
];
|
||||
|
||||
const deactivateOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['deactivate'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/workflows/{{ $value }}/deactivate',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const deleteOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/workflows/{{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getAllOperation: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 100,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 250,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
limit: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Return Only Published Workflows',
|
||||
name: 'activeWorkflows',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
active: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
// Only include the 'tags' query parameter if it's non-empty
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'tags',
|
||||
value: '={{ $value !== "" ? $value : undefined }}',
|
||||
},
|
||||
},
|
||||
description: 'Include only workflows with these tags',
|
||||
hint: 'Comma separated list of tags (empty value is ignored)',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
name: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Project ID',
|
||||
name: 'projectId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
projectId: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Exclude Pinned Data',
|
||||
name: 'excludePinnedData',
|
||||
description: 'Whether to exclude pinned data from the response',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
excludePinnedData: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/workflows/{{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getVersionOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['getVersion'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/workflows/{{ $value }}/{{ $parameter["versionId"] }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Version ID',
|
||||
name: 'versionId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['getVersion'],
|
||||
},
|
||||
},
|
||||
description: 'The version ID to retrieve',
|
||||
},
|
||||
];
|
||||
|
||||
const updateOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/workflows/{{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow Object',
|
||||
name: 'workflowObject',
|
||||
type: 'json',
|
||||
default: '',
|
||||
placeholder:
|
||||
'{\n "name": "My workflow",\n "nodes": [],\n "connections": {},\n "settings": {}\n}',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [parseAndSetBodyJson('workflowObject'), prepareWorkflowUpdateBody],
|
||||
},
|
||||
},
|
||||
description:
|
||||
"A valid JSON object with required fields: 'name', 'nodes', 'connections' and 'settings'. More information can be found in the <a href=\"https://docs.n8n.io/api/api-reference/#tag/workflow/paths/~1workflows~1%7bid%7d/put\">documentation</a>.",
|
||||
},
|
||||
];
|
||||
|
||||
export const workflowFields: INodeProperties[] = [
|
||||
...activateOperation,
|
||||
...createOperation,
|
||||
...deactivateOperation,
|
||||
...deleteOperation,
|
||||
...getAllOperation,
|
||||
...getOperation,
|
||||
...getVersionOperation,
|
||||
...updateOperation,
|
||||
];
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { ILoadOptionsFunctions, INodeListSearchResult, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { apiRequestAllItems } from './GenericFunctions';
|
||||
|
||||
type DataItemsResponse<T> = {
|
||||
data: T[];
|
||||
};
|
||||
|
||||
interface PartialWorkflow {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to populate workflow lists. It does a pseudo-search by
|
||||
* listing available workflows and matching with the specified query.
|
||||
*/
|
||||
export async function searchWorkflows(
|
||||
this: ILoadOptionsFunctions,
|
||||
query?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const searchResults = (await apiRequestAllItems.call(
|
||||
this,
|
||||
'GET',
|
||||
'workflows',
|
||||
{},
|
||||
)) as DataItemsResponse<PartialWorkflow>;
|
||||
|
||||
// Map the workflows list against a simple name/id filter, and sort
|
||||
// with the latest on top.
|
||||
const workflows = (searchResults as unknown as PartialWorkflow[])
|
||||
.map((w: PartialWorkflow) => ({
|
||||
name: `${w.name} (#${w.id})`,
|
||||
value: w.id,
|
||||
}))
|
||||
.filter(
|
||||
(w) =>
|
||||
!query ||
|
||||
w.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
w.value?.toString() === query,
|
||||
)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
return {
|
||||
results: workflows,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A resourceLocator to enable looking up workflows by their ID.
|
||||
* This object can be used as a base and then extended as needed.
|
||||
*/
|
||||
export const workflowIdLocator: INodeProperties = {
|
||||
displayName: 'Workflow',
|
||||
name: 'workflowId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
description: 'Workflow to filter the executions by',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a Workflow...',
|
||||
initType: 'workflow',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchWorkflows',
|
||||
searchFilterRequired: false,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
placeholder: 'https://myinstance.app.n8n.cloud/workflow/1',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '.*/workflow/([0-9a-zA-Z]{1,})',
|
||||
errorMessage: 'Not a valid Workflow URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: '.*/workflow/([0-9a-zA-Z]{1,})',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[0-9a-zA-Z]{1,}',
|
||||
errorMessage: 'Not a valid Workflow ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
placeholder: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 230 120"><path fill="#EA4B71" fill-rule="evenodd" d="M204 48c-11.183 0-20.58-7.649-23.244-18h-27.508a12 12 0 0 0-11.836 10.027l-.987 5.919A23.94 23.94 0 0 1 132.626 60a23.94 23.94 0 0 1 7.799 14.054l.987 5.919A12 12 0 0 0 153.248 90h3.508C159.42 79.649 168.817 72 180 72c13.255 0 24 10.745 24 24s-10.745 24-24 24c-11.183 0-20.58-7.649-23.244-18h-3.508c-11.732 0-21.744-8.482-23.673-20.054l-.987-5.919A12 12 0 0 0 116.752 66h-9.508C104.58 76.351 95.183 84 84 84s-20.58-7.649-23.244-18H47.244C44.58 76.351 35.183 84 24 84 10.745 84 0 73.255 0 60s10.745-24 24-24c11.183 0 20.58 7.649 23.244 18h13.512C63.42 43.649 72.817 36 84 36s20.58 7.649 23.244 18h9.508a12 12 0 0 0 11.836-10.027l.987-5.919C131.504 26.482 141.516 18 153.248 18h27.508C183.42 7.649 192.817 0 204 0c13.255 0 24 10.745 24 24s-10.745 24-24 24m0-12c6.627 0 12-5.373 12-12s-5.373-12-12-12-12 5.373-12 12 5.373 12 12 12M24 72c6.627 0 12-5.373 12-12s-5.373-12-12-12-12 5.373-12 12 5.373 12 12 12m72-12c0 6.627-5.373 12-12 12s-12-5.373-12-12 5.373-12 12-12 12 5.373 12 12m96 36c0 6.627-5.373 12-12 12s-12-5.373-12-12 5.373-12 12-12 12 5.373 12 12" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,20 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Test N8n Node', () => {
|
||||
const baseUrl = 'https://test.app.n8n.cloud/api/v1';
|
||||
const credentials = {
|
||||
n8nApi: {
|
||||
apiKey: 'key123',
|
||||
baseUrl,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const { pinData } = await import('./workflow.n8n.workflows.json');
|
||||
const apiResponse = pinData.n8n.map((item) => item.json);
|
||||
nock(baseUrl).get('/workflows?tags=n8n-test').reply(200, { data: apiResponse });
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
@@ -0,0 +1,694 @@
|
||||
{
|
||||
"name": "My workflow 2",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "798b312a-c922-4f11-8b96-496802bd327b",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
640,
|
||||
360
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"filters": {
|
||||
"tags": "n8n-test"
|
||||
}
|
||||
},
|
||||
"id": "90a66b97-baf9-4388-aee6-ce46dfce2749",
|
||||
"name": "n8n",
|
||||
"type": "n8n-nodes-base.n8n",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
900,
|
||||
360
|
||||
],
|
||||
"credentials": {
|
||||
"n8nApi": {
|
||||
"id": "7",
|
||||
"name": "n8n account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"n8n": [
|
||||
{
|
||||
"json": {
|
||||
"createdAt": "2023-02-07T10:30:32.416Z",
|
||||
"updatedAt": "2023-02-10T11:28:18.000Z",
|
||||
"id": "107",
|
||||
"name": "merge tests",
|
||||
"active": false,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "9f880c6f-b5e0-4453-b1d7-550beb1febe8",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
700,
|
||||
660
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {id: 1, data: 'a', input: 1, text: 'foo'},\n {id: 2, data: 'b', input: 1, text: 'foo'},\n];"
|
||||
},
|
||||
"id": "263d9c7f-32d4-4112-b271-73d09b73809a",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
900,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {id: 1, data: 'c', input: 2, tag: 'second'},\n {id: 2, data: 'd', input: 2, tag: 'second'},\n {id: 3, data: 'e', input: 2, tag: 'second'},\n];"
|
||||
},
|
||||
"id": "3defcf5a-ec5e-4d23-a1f6-a99b7362a241",
|
||||
"name": "Code1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
900,
|
||||
780
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "7120cd6f-29f3-4f5b-aa25-babebd6335d3",
|
||||
"name": "Merge",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"joinMode": "keepNonMatches",
|
||||
"options": {}
|
||||
},
|
||||
"id": "1d6973e5-0716-4d47-85ef-298d3f86bb9d",
|
||||
"name": "Merge1",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
280
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"joinMode": "keepEverything",
|
||||
"options": {}
|
||||
},
|
||||
"id": "dd75b8c0-21bf-460f-9920-644a467ed356",
|
||||
"name": "Merge2",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
420
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"joinMode": "enrichInput1",
|
||||
"options": {}
|
||||
},
|
||||
"id": "3545877d-6e4f-437e-b91f-4c792d714e73",
|
||||
"name": "Merge3",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"joinMode": "enrichInput2",
|
||||
"options": {}
|
||||
},
|
||||
"id": "4b6ac099-6e6f-4e91-bcfe-74d7524fad54",
|
||||
"name": "Merge4",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
740
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputDataFrom": "input1",
|
||||
"options": {}
|
||||
},
|
||||
"id": "fb4fef36-26ea-4fc0-b6e9-a4b2781c94fa",
|
||||
"name": "Merge5",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
900
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputDataFrom": "input2",
|
||||
"options": {}
|
||||
},
|
||||
"id": "0b4fbaaa-cf0b-4da5-b35f-da3f49409965",
|
||||
"name": "Merge6",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
1040
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {
|
||||
"values": [
|
||||
{
|
||||
"field1": "id",
|
||||
"field2": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"clashHandling": {
|
||||
"values": {
|
||||
"resolveClash": "addSuffix"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "25489a7a-74c1-48d5-a03d-1bbd2aba2abe",
|
||||
"name": "Merge7",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1320,
|
||||
1200
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Code1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge4",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge5",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge6",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge7",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge3",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge2",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge1",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge4",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge5",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge6",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"node": "Merge7",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {},
|
||||
"staticData": null,
|
||||
"pinData": {
|
||||
"Merge": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"data": "c",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"data": "d",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge1": [
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"data": "e",
|
||||
"input": 2,
|
||||
"tag": "second",
|
||||
"_source": "input2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge2": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"data": "c",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"data": "d",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"data": "e",
|
||||
"input": 2,
|
||||
"tag": "second"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge3": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"data": "c",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"data": "d",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge4": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"data": "c",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"data": "d",
|
||||
"input": 2,
|
||||
"text": "foo",
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"data": "e",
|
||||
"input": 2,
|
||||
"tag": "second"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge5": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"data": "a",
|
||||
"input": 1,
|
||||
"text": "foo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"data": "b",
|
||||
"input": 1,
|
||||
"text": "foo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge6": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"data": "c",
|
||||
"input": 2,
|
||||
"tag": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"data": "d",
|
||||
"input": 2,
|
||||
"tag": "second"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Merge7": [
|
||||
{
|
||||
"json": {
|
||||
"id_1": 1,
|
||||
"data_1": "a",
|
||||
"input_1": 1,
|
||||
"text_1": "foo",
|
||||
"id_2": 1,
|
||||
"data_2": "c",
|
||||
"input_2": 2,
|
||||
"tag_2": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id_1": 2,
|
||||
"data_1": "b",
|
||||
"input_1": 1,
|
||||
"text_1": "foo",
|
||||
"id_2": 2,
|
||||
"data_2": "d",
|
||||
"input_2": 2,
|
||||
"tag_2": "second"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"versionId": "e5d89802-2f19-42e3-8f3e-b8d5b3315227",
|
||||
"triggerCount": 0,
|
||||
"tags": [
|
||||
{
|
||||
"createdAt": "2023-02-10T11:27:50.689Z",
|
||||
"updatedAt": "2023-02-10T11:27:50.689Z",
|
||||
"id": "2",
|
||||
"name": "n8n-test"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"createdAt": "2023-02-09T13:38:39.242Z",
|
||||
"updatedAt": "2023-02-10T11:27:51.000Z",
|
||||
"id": "113",
|
||||
"name": "rss feed test",
|
||||
"active": false,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "e5fd1ef8-5528-4626-a523-7668969b2123",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
880,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://lorem-rss.herokuapp.com/feed?length=3"
|
||||
},
|
||||
"id": "9c40a026-fa38-4c91-a4a1-5332f762c3dd",
|
||||
"name": "RSS Read",
|
||||
"type": "n8n-nodes-base.rssFeedRead",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1080,
|
||||
400
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "RSS Read",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {},
|
||||
"staticData": null,
|
||||
"pinData": {
|
||||
"RSS Read": [
|
||||
{
|
||||
"json": {
|
||||
"creator": "John Smith",
|
||||
"title": "Lorem ipsum 2023-02-09T13:40:00Z",
|
||||
"link": "http://example.com/test/1675950000",
|
||||
"pubDate": "Thu, 09 Feb 2023 13:40:00 GMT",
|
||||
"dc:creator": "John Smith",
|
||||
"content": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
|
||||
"contentSnippet": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
|
||||
"guid": "http://example.com/test/1675950000",
|
||||
"isoDate": "2023-02-09T13:40:00.000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"creator": "John Smith",
|
||||
"title": "Lorem ipsum 2023-02-09T13:39:00Z",
|
||||
"link": "http://example.com/test/1675949940",
|
||||
"pubDate": "Thu, 09 Feb 2023 13:39:00 GMT",
|
||||
"dc:creator": "John Smith",
|
||||
"content": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
|
||||
"contentSnippet": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
|
||||
"guid": "http://example.com/test/1675949940",
|
||||
"isoDate": "2023-02-09T13:39:00.000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"creator": "John Smith",
|
||||
"title": "Lorem ipsum 2023-02-09T13:38:00Z",
|
||||
"link": "http://example.com/test/1675949880",
|
||||
"pubDate": "Thu, 09 Feb 2023 13:38:00 GMT",
|
||||
"dc:creator": "John Smith",
|
||||
"content": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
|
||||
"contentSnippet": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
|
||||
"guid": "http://example.com/test/1675949880",
|
||||
"isoDate": "2023-02-09T13:38:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"versionId": "c0b88b07-6bb4-4b56-b692-6eafe5f77c8d",
|
||||
"triggerCount": 0,
|
||||
"tags": [
|
||||
{
|
||||
"createdAt": "2023-02-10T11:27:50.689Z",
|
||||
"updatedAt": "2023-02-10T11:27:50.689Z",
|
||||
"id": "2",
|
||||
"name": "n8n-test"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "n8n",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "4f4c9685-5901-41d6-938f-0a45ac378dec",
|
||||
"id": "3",
|
||||
"meta": {
|
||||
"instanceId": "c9d4cba5f22cf38c90f74ed94dd16cea1f9b65fbb0fc58c6df51b44b4ca7d75d"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
Reference in New Issue
Block a user