mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat(n8nApi node): add core node for consuming the n8n API (#4076)
* feat(n8n node): create n8n core node for consuming the n8n API Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
67513e191d
commit
929315f9e4
43
packages/nodes-base/credentials/N8nApi.credentials.ts
Normal file
43
packages/nodes-base/credentials/N8nApi.credentials.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
IAuthenticateGeneric,
|
||||
ICredentialTestRequest,
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class N8nApi implements ICredentialType {
|
||||
name = 'n8nApi';
|
||||
displayName = 'n8n API';
|
||||
documentationUrl = 'n8nApi';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The API key for the n8n instance',
|
||||
},
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'baseUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'https://<name>.app.n8n.cloud/api/v1',
|
||||
description: 'The API URL of the n8n instance',
|
||||
},
|
||||
];
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
'X-N8N-API-KEY': '={{ $credentials.apiKey }}',
|
||||
},
|
||||
},
|
||||
};
|
||||
test: ICredentialTestRequest = {
|
||||
request: {
|
||||
baseURL: '={{ $credentials.baseUrl }}',
|
||||
url: '/workflows?limit=5',
|
||||
},
|
||||
};
|
||||
}
|
168
packages/nodes-base/nodes/N8n/CredentialDescription.ts
Normal file
168
packages/nodes-base/nodes/N8n/CredentialDescription.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
import { 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,
|
||||
];
|
252
packages/nodes-base/nodes/N8n/ExecutionDescription.ts
Normal file
252
packages/nodes-base/nodes/N8n/ExecutionDescription.ts
Normal file
|
@ -0,0 +1,252 @@
|
|||
/* eslint-disable n8n-nodes-base/node-param-default-missing */
|
||||
import { getCursorPaginator } from './GenericFunctions';
|
||||
import { INodeProperties } from 'n8n-workflow';
|
||||
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,
|
||||
];
|
209
packages/nodes-base/nodes/N8n/GenericFunctions.ts
Normal file
209
packages/nodes-base/nodes/N8n/GenericFunctions.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
import {
|
||||
DeclarativeRestApiSettings,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecutePaginationFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
JsonObject,
|
||||
NodeApiError,
|
||||
NodeOperationError,
|
||||
PreSendAction,
|
||||
} from 'n8n-workflow';
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
/**
|
||||
* A custom API request function to be used with the resourceLocator lookup queries.
|
||||
*/
|
||||
export async function apiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body: object,
|
||||
query?: IDataObject,
|
||||
): Promise<unknown> {
|
||||
query = query || {};
|
||||
|
||||
type N8nApiCredentials = {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const credentials = (await this.getCredentials('n8nApi')) as N8nApiCredentials;
|
||||
const baseUrl = credentials.baseUrl;
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
uri: `${baseUrl}/${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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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((page) => {
|
||||
const items = page.json.data as IDataObject[];
|
||||
if (items) {
|
||||
// Extract the items themselves
|
||||
executions = executions.concat(items.map((item) => ({ json: item })));
|
||||
}
|
||||
});
|
||||
|
||||
// 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(),
|
||||
`The '${parameterName}' property must be valid JSON, but cannot be parsed: ${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;
|
||||
};
|
22
packages/nodes-base/nodes/N8n/N8n.node.json
Normal file
22
packages/nodes-base/nodes/N8n/N8n.node.json
Normal file
|
@ -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"]
|
||||
}
|
||||
}
|
80
packages/nodes-base/nodes/N8n/N8n.node.ts
Normal file
80
packages/nodes-base/nodes/N8n/N8n.node.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
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: 'Consume n8n API',
|
||||
defaults: {
|
||||
name: 'n8n',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['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: 'Credential',
|
||||
value: 'credential',
|
||||
},
|
||||
{
|
||||
name: 'Execution',
|
||||
value: 'execution',
|
||||
},
|
||||
{
|
||||
name: 'Workflow',
|
||||
value: 'workflow',
|
||||
},
|
||||
],
|
||||
default: 'workflow',
|
||||
},
|
||||
|
||||
...credentialOperations,
|
||||
...credentialFields,
|
||||
|
||||
...executionOperations,
|
||||
...executionFields,
|
||||
|
||||
...workflowOperations,
|
||||
...workflowFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch: {
|
||||
// Provide workflows search capability for the workflow resourceLocator
|
||||
searchWorkflows,
|
||||
},
|
||||
};
|
||||
}
|
323
packages/nodes-base/nodes/N8n/WorkflowDescription.ts
Normal file
323
packages/nodes-base/nodes/N8n/WorkflowDescription.ts
Normal file
|
@ -0,0 +1,323 @@
|
|||
import { 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: 'Activate',
|
||||
value: 'activate',
|
||||
action: 'Activate a workflow',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
action: 'Create a workflow',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/workflows',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Deactivate',
|
||||
value: 'deactivate',
|
||||
action: 'Deactivate 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: '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',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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 Active 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)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getOperation: INodeProperties[] = [
|
||||
{
|
||||
...workflowIdLocator,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workflow'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/workflows/{{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
...updateOperation,
|
||||
];
|
106
packages/nodes-base/nodes/N8n/WorkflowLocator.ts
Normal file
106
packages/nodes-base/nodes/N8n/WorkflowLocator.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { ILoadOptionsFunctions, INodeListSearchResult, INodeProperties } from 'n8n-workflow';
|
||||
import { apiRequest } 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 apiRequest.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?.data 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-9]{1,})',
|
||||
errorMessage: 'Not a valid Workflow URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: '.*/workflow/([0-9]{1,})',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[0-9]{1,}',
|
||||
errorMessage: 'Not a valid Workflow ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
placeholder: '1',
|
||||
},
|
||||
],
|
||||
};
|
1
packages/nodes-base/nodes/N8n/n8n.svg
Normal file
1
packages/nodes-base/nodes/N8n/n8n.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5 5 55 55"><path d="M50.28 17.26c-2.39 0-4.4 1.64-4.97 3.86h-7.15c-2.8 0-5.08 2.28-5.08 5.08 0 1.4-1.14 2.54-2.54 2.54h-1.02a5.138 5.138 0 00-4.97-3.86c-2.39 0-4.4 1.64-4.97 3.86H15.5a5.138 5.138 0 00-4.97-3.86c-2.83 0-5.13 2.3-5.13 5.13s2.3 5.13 5.13 5.13c2.39 0 4.4-1.64 4.97-3.86h4.08a5.138 5.138 0 004.97 3.86 5.13 5.13 0 004.95-3.81h1.03c1.4 0 2.54 1.14 2.54 2.54 0 2.8 2.28 5.09 5.08 5.09h1.66a5.138 5.138 0 004.97 3.86c2.83 0 5.13-2.3 5.13-5.13s-2.3-5.13-5.13-5.13c-2.39 0-4.4 1.64-4.97 3.86h-1.66c-1.4 0-2.54-1.14-2.54-2.54 0-1.53-.68-2.9-1.76-3.84a5.092 5.092 0 001.76-3.84c0-1.4 1.14-2.54 2.54-2.54h7.15a5.138 5.138 0 004.97 3.86c2.83 0 5.13-2.3 5.13-5.13s-2.29-5.13-5.12-5.13zM10.54 32.61a2.59 2.59 0 11.001-5.181 2.59 2.59 0 01-.001 5.181zm14.02 0a2.59 2.59 0 112.59-2.59c0 1.43-1.17 2.59-2.59 2.59zm20.24 2.5a2.59 2.59 0 11-.001 5.181 2.59 2.59 0 01.001-5.181zm5.48-10.13a2.59 2.59 0 11.001-5.181 2.59 2.59 0 01-.001 5.181z" fill="#ff6d5a"/></svg>
|
After Width: | Height: | Size: 1,009 B |
|
@ -208,6 +208,7 @@
|
|||
"dist/credentials/Mqtt.credentials.js",
|
||||
"dist/credentials/Msg91Api.credentials.js",
|
||||
"dist/credentials/MySql.credentials.js",
|
||||
"dist/credentials/N8nApi.credentials.js",
|
||||
"dist/credentials/NasaApi.credentials.js",
|
||||
"dist/credentials/NetlifyApi.credentials.js",
|
||||
"dist/credentials/NextCloudApi.credentials.js",
|
||||
|
@ -556,6 +557,7 @@
|
|||
"dist/nodes/MQTT/MqttTrigger.node.js",
|
||||
"dist/nodes/Msg91/Msg91.node.js",
|
||||
"dist/nodes/MySql/MySql.node.js",
|
||||
"dist/nodes/N8n/N8n.node.js",
|
||||
"dist/nodes/N8nTrainingCustomerDatastore/N8nTrainingCustomerDatastore.node.js",
|
||||
"dist/nodes/N8nTrainingCustomerMessenger/N8nTrainingCustomerMessenger.node.js",
|
||||
"dist/nodes/N8nTrigger/N8nTrigger.node.js",
|
||||
|
|
Loading…
Reference in a new issue