🔀 Merge branch 'master' into oauth-support

This commit is contained in:
Jan Oberhauser 2020-04-14 20:54:19 +02:00
commit 90fe5113a0
35 changed files with 1201 additions and 449 deletions

View file

@ -60,12 +60,12 @@ export DB_POSTGRESDB_SCHEMA=n8n
n8n start
```
## MySQL
## MySQL / MariaDB
The compatibility with MySQL was tested, even so, it is advisable to observe the operation of the application with this DB, as it is a new option, recently added. If you spot any problems, feel free to submit a PR.
The compatibility with MySQL/MariaDB was tested, even so, it is advisable to observe the operation of the application with this DB, as it is a new option, recently added. If you spot any problems, feel free to submit a PR.
To use MySQL as database you can provide the following environment variables:
- `DB_TYPE=mysqldb`
- `DB_TYPE=mysqldb` or `DB_TYPE=mariadb`
- `DB_MYSQLDB_DATABASE` (default: 'n8n')
- `DB_MYSQLDB_HOST` (default: 'localhost')
- `DB_MYSQLDB_PORT` (default: 3306)

View file

@ -48,11 +48,14 @@ the value would be: "My name is: Jim"
The following special variables are available:
- **$binary**: Incoming binary data of a node
- **$data**: Incoming JSON data of a node
- **$evaluateExpression**: Evaluates a string as expression
- **$env**: Environment variables
- **$node**: Data of other nodes (output-data, parameters)
- **$items**: Environment variables
- **$json**: Incoming JSON data of a node
- **$node**: Data of other nodes (binary, context, json, parameter, runIndex)
- **$parameters**: Parameters of the current node
- **$runIndex**: The current run index (first time node gets executed it is 0, second time 1, ...)
- **$workflow**: Returns workflow metadata like: active, id, name
Normally it is not needed to write the JavaScript variables manually as they can be simply selected with the help of the Expression Editor.

View file

@ -59,7 +59,7 @@ return newItems;
```
#### Method: $item(index)
#### Method: $item(index: number, runIndex?: number)
With `$item` it is possible to access the data of parent nodes. That can be the item data but also
the parameters. It expects as input an index of the item the data should be returned for. This is
@ -71,6 +71,12 @@ emails at once to different people. Instead, the same person would receive multi
The index is 0 based. So `$item(0)` will return the first item, `$item(1)` the second one, ...
By default will the item of the last run of the node be returned. So if the referenced node did run
3x (its last runIndex is 2) and the current node runs the first time (its runIndex is 0) will the
data of runIndex 2 of the referenced node be returned.
For more information about what data can be accessed via $node check [here](#variable-node).
Example:
```typescript
@ -88,18 +94,76 @@ const channel = $item(9).$node["Slack"].parameter["channel"];
```
#### Variable: $node
#### Method: $items(nodeName?: string, outputIndex?: number, runIndex?: number)
Works exactly like `$item` with the difference that it will always return the data of the first item.
Gives access to all the items of current or parent nodes. If no parameters get supplied
it returns all the items of the current node.
If a node-name is given, it returns the items the node did output on it`s first output
(index: 0, most nodes only have one output, exceptions are IF and Switch-Node) on
its last run.
Example:
```typescript
const myNumber = $node["Set"].json['myNumber'];
// Returns all the items of the current node and current run
const allItems = $items();
const channel = $node["Slack"].parameter["channel"];
// Returns all items the node "IF" outputs (index: 0 which is Output "true" of its most recent run)
const allItems = $items("IF");
// Returns all items the node "IF" outputs (index: 0 which is Output "true" of the same run as current node)
const allItems = $items("IF", 0, $runIndex);
// Returns all items the node "IF" outputs (index: 1 which is Output "false" of run 0 which is the first run)
const allItems = $items("IF", 1, 0);
```
#### Method: evaluateExpression(expression: string, itemIndex: number)
#### Variable: $node
Works exactly like `$item` with the difference that it will always return the data of the first item and
the last run of the node.
```typescript
// Returns the fileName of binary property "data" of Node "HTTP Request"
const fileName = $node["HTTP Request"].binary["data"]["fileName"]}}
// Returns the context data "noItemsLeft" of Node "SplitInBatches"
const noItemsLeft = $node["SplitInBatches"].context["noItemsLeft"];
// Returns the value of the JSON data property "myNumber" of Node "Set"
const myNumber = $node["Set"].json['myNumber'];
// Returns the value of the parameter "channel" of Node "Slack"
const channel = $node["Slack"].parameter["channel"];
// Returns the index of the last run of Node "HTTP Request"
const runIndex = $node["HTTP Request"].runIndex}}
```
#### Variable: $runIndex
Contains the index of the current run of the node.
```typescript
// Returns all items the node "IF" outputs (index: 0 which is Output "true" of the same run as current node)
const allItems = $items("IF", 0, $runIndex);
```
#### Variable: $workflow
Gives information about the current workflow.
```typescript
const isActive = $workflow.active;
const workflowId = $workflow.id;
const workflowName = $workflow.name;
```
#### Method: $evaluateExpression(expression: string, itemIndex: number)
Evaluates a given string as expression.
If no `itemIndex` is provided it uses by default in the Function-Node the data of item 0 and
@ -108,8 +172,8 @@ in the Function Item-Node the data of the current item.
Example:
```javascript
items[0].json.variable1 = evaluateExpression('{{1+2}}');
items[0].json.variable2 = evaluateExpression($node["Set"].json["myExpression"], 1);
items[0].json.variable1 = $evaluateExpression('{{1+2}}');
items[0].json.variable2 = $evaluateExpression($node["Set"].json["myExpression"], 1);
return items;
```

View file

@ -2,6 +2,7 @@
This list shows all the versions which include breaking changes and how to upgrade
## ???
### What changed?
@ -30,6 +31,23 @@ it has to get changed to:
```
## 0.62.0
### What changed?
The function "evaluateExpression(...)" got renamed to "$evaluateExpression()"
in Function and FunctionItem Nodes to simplify code and to normalize function
names.
### When is action necessary?
If "evaluateExpression(...)" gets used in any Function or FunctionItem Node.
### How to upgrade:
Simply replace the "evaluateExpression(...)" with "$evaluateExpression(...)".
## 0.52.0
### What changed?

View file

@ -8,7 +8,7 @@ const config = convict({
database: {
type: {
doc: 'Type of database to use',
format: ['sqlite', 'mongodb', 'mysqldb', 'postgresdb'],
format: ['sqlite', 'mariadb', 'mongodb', 'mysqldb', 'postgresdb'],
default: 'sqlite',
env: 'DB_TYPE'
},

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.60.0",
"version": "0.62.1",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -97,10 +97,10 @@
"lodash.get": "^4.4.2",
"mongodb": "^3.2.3",
"mysql2": "^2.0.1",
"n8n-core": "~0.29.0",
"n8n-editor-ui": "~0.40.0",
"n8n-nodes-base": "~0.55.0",
"n8n-workflow": "~0.26.0",
"n8n-core": "~0.31.0",
"n8n-editor-ui": "~0.42.0",
"n8n-nodes-base": "~0.57.1",
"n8n-workflow": "~0.28.0",
"open": "^7.0.0",
"pg": "^7.11.0",
"request-promise-native": "^1.0.7",

View file

@ -63,11 +63,12 @@ export async function init(synchronize?: boolean): Promise<IDatabaseCollections>
};
break;
case 'mariadb':
case 'mysqldb':
dbNotExistError = 'does not exist';
entities = MySQLDb;
connectionOptions = {
type: 'mysql',
type: dbType === 'mysqldb' ? 'mysql' : 'mariadb',
database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string,
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
host: await GenericHelpers.getConfigValue('database.mysqldb.host') as string,

View file

@ -91,7 +91,7 @@ export interface ICredentialsDecryptedResponse extends ICredentialsDecryptedDb {
id: string;
}
export type DatabaseType = 'mongodb' | 'postgresdb' | 'mysqldb' | 'sqlite';
export type DatabaseType = 'mariadb' | 'mongodb' | 'postgresdb' | 'mysqldb' | 'sqlite';
export type SaveExecutionDataType = 'all' | 'none';
export interface IExecutionBase {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.29.0",
"version": "0.31.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -45,7 +45,7 @@
"crypto-js": "3.1.9-1",
"lodash.get": "^4.4.2",
"mmmagic": "^0.5.2",
"n8n-workflow": "~0.26.0",
"n8n-workflow": "~0.28.0",
"p-cancelable": "^2.0.0",
"request-promise-native": "^1.0.7"
},

View file

@ -596,6 +596,17 @@ export class WorkflowExecute {
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
if (nodeSuccessData === null || nodeSuccessData[0][0] === undefined) {
if (executionData.node.alwaysOutputData === true) {
nodeSuccessData = nodeSuccessData || [];
nodeSuccessData[0] = [
{
json: {},
}
];
}
}
if (nodeSuccessData === null) {
// If null gets returned it means that the node did succeed
// but did not have any data. So the branch should end

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.40.0",
"version": "0.42.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -64,7 +64,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.26.0",
"n8n-workflow": "~0.28.0",
"node-sass": "^4.12.0",
"prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3",

View file

@ -141,6 +141,7 @@ export default mixins(
nodeColor: null,
nodeValues: {
color: '#ff0000',
alwaysOutputData: false,
continueOnFail: false,
retryOnFail: false,
maxTries: 3,
@ -169,6 +170,14 @@ export default mixins(
noDataExpression: true,
description: 'The color of the node in the flow.',
},
{
displayName: 'Always Output Data',
name: 'alwaysOutputData',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'If activated and the node does not have any data for the first output,<br />it returns an empty item anyway. Be careful setting this on<br />IF-Nodes as it could easily cause an infinite loop.',
},
{
displayName: 'Retry On Fail',
name: 'retryOnFail',
@ -419,6 +428,11 @@ export default mixins(
Vue.set(this.nodeValues, 'notes', this.node.notes);
}
if (this.node.alwaysOutputData) {
foundNodeSettings.push('alwaysOutputData');
Vue.set(this.nodeValues, 'alwaysOutputData', this.node.alwaysOutputData);
}
if (this.node.continueOnFail) {
foundNodeSettings.push('continueOnFail');
Vue.set(this.nodeValues, 'continueOnFail', this.node.continueOnFail);

View file

@ -21,6 +21,7 @@ import {
formOperations
} from './FormDescription';
import { submitForm } from './FormFunctions';
import { createDataFromParameters } from './GenericFunctions';
import {
singletonFields,
singletonOperations,
@ -56,7 +57,7 @@ export class Cockpit implements INodeType {
displayName: 'Resource',
name: 'resource',
type: 'options',
default: 'collections',
default: 'collection',
description: 'Resource to consume.',
options: [
{
@ -74,7 +75,6 @@ export class Cockpit implements INodeType {
],
},
...collectionOperations,
...collectionFields,
...formOperations,
@ -84,7 +84,6 @@ export class Cockpit implements INodeType {
],
};
methods = {
loadOptions: {
async getCollections(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
@ -123,34 +122,37 @@ export class Cockpit implements INodeType {
for (let i = 0; i < length; i++) {
if (resource === 'collection') {
const collectionName = this.getNodeParameter('collection', i) as string;
if (operation === 'create') {
const data = this.getNodeParameter('data', i) as IDataObject;
const data = createDataFromParameters.call(this, i);
responseData = await createCollectionEntry.call(this, collectionName, data);
} else if (operation === 'getAll') {
const options = this.getNodeParameter('options', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll !== true) {
if (!returnAll) {
options.limit = this.getNodeParameter('limit', i) as number;
}
responseData = await getAllCollectionEntries.call(this, collectionName, options);
} else if (operation === 'update') {
const id = this.getNodeParameter('id', i) as string;
const data = this.getNodeParameter('data', i) as IDataObject;
const data = createDataFromParameters.call(this, i);
responseData = await createCollectionEntry.call(this, collectionName, data, id);
}
} else if (resource === 'form') {
const formName = this.getNodeParameter('form', i) as string;
if (operation === 'submit') {
const form = this.getNodeParameter('form', i) as IDataObject;
const form = createDataFromParameters.call(this, i);
responseData = await submitForm.call(this, formName, form);
}
} else if (resource === 'singleton') {
const singletonName = this.getNodeParameter('singleton', i) as string;
if (operation === 'get') {
responseData = await getSingleton.call(this, singletonName);
}

View file

@ -14,17 +14,17 @@ export const collectionOperations = [
},
options: [
{
name: 'Create an entry',
name: 'Create an Entry',
value: 'create',
description: 'Create a collection entry',
},
{
name: 'Get all entries',
name: 'Get all Entries',
value: 'getAll',
description: 'Get all collection entries',
},
{
name: 'Update an entry',
name: 'Update an Entry',
value: 'update',
description: 'Update a collection entries',
},
@ -54,29 +54,6 @@ export const collectionFields = [
description: 'Name of the collection to operate on.'
},
// Collection:entry:create
{
displayName: 'Data',
name: 'data',
type: 'json',
required: true,
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: [
'collection',
],
operation: [
'create',
]
},
},
description: 'The data to create.',
},
// Collection:entry:getAll
{
displayName: 'Return All',
@ -139,22 +116,24 @@ export const collectionFields = [
{
displayName: 'Fields',
name: 'fields',
type: 'json',
type: 'string',
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
description: 'Fields to get.',
placeholder: '_id,name',
description: 'Comma separated list of fields to get.',
},
{
displayName: 'Filter',
displayName: 'Filter Query',
name: 'filter',
type: 'json',
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
description: 'Filter result by fields.',
placeholder: '{"name": "Jim"}',
description: 'Filter query in <a href="https://jeroen.github.io/mongolite/query-data.html" target="_blank">Mongolite format</a>.',
},
{
displayName: 'Language',
@ -186,11 +165,12 @@ export const collectionFields = [
description: 'Skip number of entries.',
},
{
displayName: 'Sort',
displayName: 'Sort Query',
name: 'sort',
type: 'json',
default: '',
description: 'Sort result by fields.',
placeholder: '{"price": -1}',
description: 'Sort query in <a href="https://jeroen.github.io/mongolite/query-data.html" target="_blank">Mongolite format</a>.',
},
],
},
@ -214,25 +194,95 @@ export const collectionFields = [
},
description: 'The entry ID.',
},
// Collection:entry:create
// Collection:entry:update
{
displayName: 'Data',
name: 'data',
type: 'json',
required: true,
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayName: 'JSON Data fields',
name: 'jsonDataFields',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'collection',
],
operation: [
'create',
'update',
]
},
},
description: 'The data to update.',
description: 'If new entry fields should be set via the value-key pair UI or JSON.',
},
{
displayName: 'Entry Data',
name: 'dataFieldsJson',
type: 'json',
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
jsonDataFields: [
true,
],
resource: [
'collection',
],
operation: [
'create',
'update',
]
},
},
description: 'Entry data to send as JSON.',
},
{
displayName: 'Entry Data',
name: 'dataFieldsUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
jsonDataFields: [
false,
],
resource: [
'collection',
],
operation: [
'create',
'update',
]
},
},
options: [
{
displayName: 'Field',
name: 'field',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the field.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field.',
},
],
},
],
description: 'Entry data to send.',
},
] as INodeProperties[];

View file

@ -9,7 +9,7 @@ import { cockpitApiRequest } from './GenericFunctions';
export async function createCollectionEntry(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, data: IDataObject, id?: string): Promise<any> { // tslint:disable-line:no-any
const body: ICollection = {
data: JSON.parse(data.toString())
data,
};
if (id) {
@ -27,7 +27,16 @@ export async function getAllCollectionEntries(this: IExecuteFunctions | IExecute
const body: ICollection = {};
if (options.fields) {
body.fields = JSON.parse(options.fields.toString());
const fields = (options.fields as string).split(',').map(field => field.trim() );
const bodyFields = {
_id: false,
} as IDataObject;
for (const field of fields) {
bodyFields[field] = true;
}
body.fields = bodyFields;
}
if (options.filter) {

View file

@ -14,7 +14,7 @@ export const formOperations = [
},
options: [
{
name: 'Submit a form',
name: 'Submit a Form',
value: 'submit',
description: 'Store submission of a form',
},
@ -44,21 +44,88 @@ export const formFields = [
// Form:submit
{
displayName: 'Form data',
name: 'form',
displayName: 'JSON Data fields',
name: 'jsonDataFields',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'submit',
]
},
},
description: 'If form fields should be set via the value-key pair UI or JSON.',
},
{
displayName: 'Form Data',
name: 'dataFieldsJson',
type: 'json',
required: true,
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
jsonDataFields: [
true,
],
resource: [
'form',
],
operation: [
'submit',
]
},
},
description: 'The data to save.',
description: 'Form data to send as JSON.',
},
{
displayName: 'Form Data',
name: 'dataFieldsUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
jsonDataFields: [
false,
],
resource: [
'form',
],
operation: [
'submit',
]
},
},
options: [
{
displayName: 'Field',
name: 'field',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the field.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field.',
},
],
},
],
description: 'Form data to send.',
},
] as INodeProperties[];

View file

@ -9,7 +9,7 @@ import { cockpitApiRequest } from './GenericFunctions';
export async function submitForm(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, form: IDataObject) {
const body: IForm = {
form: JSON.parse(form.toString())
form
};
return cockpitApiRequest.call(this, 'post', `/forms/submit/${resourceName}`, body);

View file

@ -44,3 +44,26 @@ export async function cockpitApiRequest(this: IExecuteFunctions | IExecuteSingle
throw new Error(`Cockpit error [${error.statusCode}]: ` + errorMessage);
}
}
export function createDataFromParameters(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, itemIndex: number): IDataObject {
const dataFieldsAreJson = this.getNodeParameter('jsonDataFields', itemIndex) as boolean;
if (dataFieldsAreJson) {
// Parameters are defined as JSON
return JSON.parse(this.getNodeParameter('dataFieldsJson', itemIndex, {}) as string);
}
// Parameters are defined in UI
const uiDataFields = this.getNodeParameter('dataFieldsUi', itemIndex, {}) as IDataObject;
const unpacked: IDataObject = {};
if (uiDataFields.field === undefined) {
return unpacked;
}
for (const field of uiDataFields!.field as IDataObject[]) {
unpacked[field!.name as string] = field!.value;
}
return unpacked;
}

View file

@ -16,7 +16,7 @@ export const singletonOperations = [
{
name: 'Get',
value: 'get',
description: 'Gets a singleton',
description: 'Gets a Singleton',
},
],
default: 'get',

View file

@ -47,9 +47,6 @@ export class Function implements INodeType {
// Define the global objects for the custom function
const sandbox = {
evaluateExpression: (expression: string, itemIndex = 0) => {
return this.evaluateExpression(expression, itemIndex);
},
getNodeParameter: this.getNodeParameter,
getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,

View file

@ -48,9 +48,6 @@ export class FunctionItem implements INodeType {
// Define the global objects for the custom function
const sandbox = {
evaluateExpression: (expression: string, itemIndex: number | undefined) => {
return this.evaluateExpression(expression, itemIndex);
},
getBinaryData: (): IBinaryKeyData | undefined => {
return item.binary;
},

View file

@ -713,12 +713,8 @@ export class HttpRequest implements INodeType {
}
}
try {
// @ts-ignore
requestOptions[optionData.name] = JSON.parse(tempValue as string);
} catch (error) {
throw new Error(`${optionData.name} must be a valid JSON`);
}
// @ts-ignore
requestOptions[optionData.name] = tempValue;
// @ts-ignore
if (typeof requestOptions[optionData.name] !== 'object' && options.bodyContentType !== 'raw') {

View file

@ -7,14 +7,15 @@ import {
import { OptionsWithUri } from 'request';
import { IDataObject } from 'n8n-workflow';
export interface IAttachment {
export interface IAttachment {
fields: {
item?: object[];
};
actions: {
item?: object[];
};
}
/**
* Make an API request to Telegram
*

View file

@ -448,78 +448,181 @@ export class Mattermost implements INodeType {
placeholder: 'Add attachment item',
options: [
{
displayName: 'Fallback Text',
name: 'fallback',
type: 'string',
displayName: 'Actions',
name: 'actions',
placeholder: 'Add Actions',
description: 'Actions to add to message. More information can be found <a href="https://docs.mattermost.com/developer/interactive-messages.html" target="_blank">here</a>',
type: 'fixedCollection',
typeOptions: {
alwaysOpenEditWindow: true,
multipleValues: true,
},
default: '',
description: 'Required plain-text summary of the attachment.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text to send.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Title of the message.',
},
{
displayName: 'Title Link',
name: 'title_link',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Link of the title.',
},
{
displayName: 'Color',
name: 'color',
type: 'color',
default: '#ff0000',
description: 'Color of the line left of text.',
},
{
displayName: 'Pretext',
name: 'pretext',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text which appears before the message block.',
},
{
displayName: 'Author Name',
name: 'author_name',
type: 'string',
default: '',
description: 'Name that should appear.',
},
{
displayName: 'Author Link',
name: 'author_link',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Link for the author.',
default: {},
options: [
{
displayName: 'Item',
name: 'item',
values: [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Button',
value: 'button',
},
{
name: 'Select',
value: 'select',
},
],
default: 'button',
description: 'The type of the action.',
},
{
displayName: 'Data Source',
name: 'data_source',
type: 'options',
displayOptions: {
show: {
type: [
'select'
],
},
},
options: [
{
name: 'Channels',
value: 'channels',
},
{
name: 'Custom',
value: 'custom',
},
{
name: 'Users',
value: 'users',
},
],
default: 'custom',
description: 'The type of the action.',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Adds a new option to select field.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
data_source: [
'custom'
],
type: [
'select'
],
},
},
default: {},
options: [
{
name: 'option',
displayName: 'Option',
default: {},
values: [
{
displayName: 'Option Text',
name: 'text',
type: 'string',
default: '',
description: 'Text of the option.',
},
{
displayName: 'Option Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the option.',
},
]
},
],
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the Action.',
},
{
displayName: 'Integration',
name: 'integration',
placeholder: 'Add Integration',
description: 'Integration to add to message.',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Item',
name: 'item',
default: {},
values: [
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
description: 'URL of the Integration.',
},
{
displayName: 'Context',
name: 'context',
placeholder: 'Add Context to Integration',
description: 'Adds a Context values set.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'property',
displayName: 'Property',
default: {},
values: [
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the property to set.',
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the property to set.',
},
]
},
],
},
]
},
],
},
]
},
],
},
{
displayName: 'Author Icon',
@ -532,44 +635,38 @@ export class Mattermost implements INodeType {
description: 'Icon which should appear for the user.',
},
{
displayName: 'Image URL',
name: 'image_url',
displayName: 'Author Link',
name: 'author_link',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'URL of image.',
description: 'Link for the author.',
},
{
displayName: 'Thumbnail URL',
name: 'thumb_url',
displayName: 'Author Name',
name: 'author_name',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'URL of thumbnail.',
description: 'Name that should appear.',
},
{
displayName: 'Footer',
name: 'footer',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text of footer to add.',
displayName: 'Color',
name: 'color',
type: 'color',
default: '#ff0000',
description: 'Color of the line left of text.',
},
{
displayName: 'Footer Icon',
name: 'footer_icon',
displayName: 'Fallback Text',
name: 'fallback',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Icon which should appear next to footer.',
description: 'Required plain-text summary of the attachment.',
},
{
displayName: 'Fields',
@ -610,7 +707,87 @@ export class Mattermost implements INodeType {
]
},
],
}
},
{
displayName: 'Footer',
name: 'footer',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text of footer to add.',
},
{
displayName: 'Footer Icon',
name: 'footer_icon',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Icon which should appear next to footer.',
},
{
displayName: 'Image URL',
name: 'image_url',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'URL of image.',
},
{
displayName: 'Pretext',
name: 'pretext',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text which appears before the message block.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Text to send.',
},
{
displayName: 'Thumbnail URL',
name: 'thumb_url',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'URL of thumbnail.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Title of the message.',
},
{
displayName: 'Title Link',
name: 'title_link',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Link of the title.',
},
],
},
{
@ -890,6 +1067,47 @@ export class Mattermost implements INodeType {
}
}
}
for (const attachment of attachments) {
if (attachment.actions !== undefined) {
if (attachment.actions.item !== undefined) {
// Move the field-content up
// @ts-ignore
attachment.actions = attachment.actions.item;
} else {
// If it does not have any items set remove it
delete attachment.actions;
}
}
}
for (const attachment of attachments) {
if (Array.isArray(attachment.actions)) {
for (const attaction of attachment.actions) {
if (attaction.type === 'button') {
delete attaction.type;
}
if (attaction.data_source === 'custom') {
delete attaction.data_source;
}
if (attaction.options) {
attaction.options = attaction.options.option;
}
if (attaction.integration.item !== undefined) {
attaction.integration = attaction.integration.item;
if (Array.isArray(attaction.integration.context.property)) {
const tmpcontex = {};
for (const attactionintegprop of attaction.integration.context.property) {
Object.assign(tmpcontex, { [attactionintegprop.name]: attactionintegprop.value });
}
delete attaction.integration.context;
attaction.integration.context = tmpcontex;
}
}
}
}
}
body.props = {
attachments,

View file

@ -28,7 +28,7 @@ import {
function flattenObject (data: IDataObject) {
const returnData: IDataObject = {};
for (const key1 of Object.keys(data)) {
if ((typeof data[key1]) === 'object') {
if (data[key1] !== null && (typeof data[key1]) === 'object') {
const flatObject = flattenObject(data[key1] as IDataObject);
for (const key2 in flatObject) {
if (flatObject[key2] === undefined) {
@ -133,6 +133,11 @@ export class SpreadsheetFile implements INodeType {
value: 'xls',
description: 'Excel',
},
{
name: 'XLSX',
value: 'xlsx',
description: 'Excel',
},
],
default: 'xls',
displayOptions: {
@ -236,6 +241,7 @@ export class SpreadsheetFile implements INodeType {
'/fileFormat': [
'ods',
'xls',
'xlsx',
],
},
},
@ -337,7 +343,9 @@ export class SpreadsheetFile implements INodeType {
} else if (fileFormat === 'ods') {
wopts.bookType = 'ods';
} else if (fileFormat === 'xls') {
wopts.bookType = 'xlml';
wopts.bookType = 'xls';
} else if (fileFormat === 'xlsx') {
wopts.bookType = 'xlsx';
}
// Convert the data in the correct format

View file

@ -62,6 +62,9 @@ export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteF
responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri);
uri = responseData.next_page;
returnData.push.apply(returnData, responseData[propertyName]);
if (query.limit && query.limit <= returnData.length) {
return returnData;
}
} while (
responseData.next_page !== undefined &&
responseData.next_page !== null
@ -69,3 +72,13 @@ export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteF
return returnData;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}

View file

@ -21,9 +21,9 @@ export const ticketOperations = [
description: 'Create a ticket',
},
{
name: 'Update',
value: 'update',
description: 'Update a ticket',
name: 'Delete',
value: 'delete',
description: 'Delete a ticket',
},
{
name: 'Get',
@ -36,9 +36,9 @@ export const ticketOperations = [
description: 'Get all tickets',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a ticket',
name: 'Update',
value: 'update',
description: 'Update a ticket',
},
],
default: 'create',
@ -81,7 +81,7 @@ export const ticketFields = [
displayOptions: {
show: {
resource: [
'ticket'
'ticket',
],
operation: [
'create',
@ -103,9 +103,47 @@ export const ticketFields = [
operation: [
'create',
],
jsonParameters: [
false,
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Custom Field',
name: 'customFieldsValues',
values: [
{
displayName: 'ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
default: '',
description: 'Custom field ID',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Custom field Value.',
},
],
},
],
},
{
displayName: 'External ID',
name: 'externalId',
@ -113,20 +151,6 @@ export const ticketFields = [
default: '',
description: 'An id you can use to link Zendesk Support tickets to local records',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
description: 'The value of the subject field for this ticket',
},
{
displayName: 'Recipient',
name: 'recipient',
type: 'string',
default: '',
description: 'The original recipient e-mail address of the ticket',
},
{
displayName: 'Group',
name: 'group',
@ -137,6 +161,49 @@ export const ticketFields = [
default: '',
description: 'The group this ticket is assigned to',
},
{
displayName: 'Recipient',
name: 'recipient',
type: 'string',
default: '',
description: 'The original recipient e-mail address of the ticket',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Open',
value: 'open',
},
{
name: 'New',
value: 'new',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
{
name: 'Closed',
value: 'closed',
},
],
default: '',
description: 'The state of the ticket',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
description: 'The value of the subject field for this ticket',
},
{
displayName: 'Tags',
name: 'tags',
@ -172,40 +239,11 @@ export const ticketFields = [
default: '',
description: 'The type of this ticket',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Open',
value: 'open',
},
{
name: 'New',
value: 'new',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
{
name: 'Closed',
value: 'closed',
},
],
default: '',
description: 'The state of the ticket',
}
],
},
{
displayName: ' Custom Fields',
name: 'customFieldsJson',
displayName: ' Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
@ -224,14 +262,14 @@ export const ticketFields = [
],
},
},
required: true,
description: `Array of customs fields <a href="https://developer.zendesk.com/rest_api/docs/support/tickets#setting-custom-field-values" target="_blank">Details</a>`,
description: `Object of values to set as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets" target="_blank">here</a>.`,
},
/* -------------------------------------------------------------------------- */
/* ticket:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'ID',
displayName: 'Ticket ID',
name: 'id',
type: 'string',
default: '',
@ -279,9 +317,47 @@ export const ticketFields = [
operation: [
'update',
],
jsonParameters: [
false,
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Custom Field',
name: 'customFieldsValues',
values: [
{
displayName: 'ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
default: '',
description: 'Custom field ID',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Custom field Value.',
},
],
},
],
},
{
displayName: 'External ID',
name: 'externalId',
@ -289,20 +365,6 @@ export const ticketFields = [
default: '',
description: 'An id you can use to link Zendesk Support tickets to local records',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
description: 'The value of the subject field for this ticket',
},
{
displayName: 'Recipient',
name: 'recipient',
type: 'string',
default: '',
description: 'The original recipient e-mail address of the ticket',
},
{
displayName: 'Group',
name: 'group',
@ -313,6 +375,49 @@ export const ticketFields = [
default: '',
description: 'The group this ticket is assigned to',
},
{
displayName: 'Recipient',
name: 'recipient',
type: 'string',
default: '',
description: 'The original recipient e-mail address of the ticket',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Open',
value: 'open',
},
{
name: 'New',
value: 'new',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
{
name: 'Closed',
value: 'closed',
},
],
default: '',
description: 'The state of the ticket',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
description: 'The value of the subject field for this ticket',
},
{
displayName: 'Tags',
name: 'tags',
@ -348,40 +453,11 @@ export const ticketFields = [
default: '',
description: 'The type of this ticket',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Open',
value: 'open',
},
{
name: 'New',
value: 'new',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
{
name: 'Closed',
value: 'closed',
},
],
default: '',
description: 'The state of the ticket',
}
],
},
{
displayName: ' Custom Fields',
name: 'customFieldsJson',
displayName: ' Update Fields',
name: 'updateFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
@ -400,14 +476,14 @@ export const ticketFields = [
],
},
},
required: true,
description: `Array of customs fields <a href='https://developer.zendesk.com/rest_api/docs/support/tickets#setting-custom-field-values'>Details</a>`,
description: `Object of values to update as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets" target="_blank">here</a>.`,
},
/* -------------------------------------------------------------------------- */
/* ticket:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'ID',
displayName: 'Ticket ID',
name: 'id',
type: 'string',
default: '',
@ -485,35 +561,6 @@ export const ticketFields = [
},
},
options: [
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Open',
value: 'open',
},
{
name: 'New',
value: 'new',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
{
name: 'Closed',
value: 'closed',
},
],
default: '',
description: 'The state of the ticket',
},
{
displayName: 'Sort By',
name: 'sortBy',
@ -559,7 +606,36 @@ export const ticketFields = [
],
default: 'desc',
description: 'Sort order',
}
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Open',
value: 'open',
},
{
name: 'New',
value: 'new',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
{
name: 'Closed',
value: 'closed',
},
],
default: '',
description: 'The state of the ticket',
},
],
},
@ -567,7 +643,7 @@ export const ticketFields = [
/* ticket:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'ID',
displayName: 'Ticket ID',
name: 'id',
type: 'string',
default: '',

View file

@ -54,4 +54,49 @@ export const ticketFieldFields = [
},
description: 'ticketField ID',
},
/* -------------------------------------------------------------------------- */
/* ticketField:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'ticketField',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'ticketField',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -12,6 +12,7 @@ import {
} from 'n8n-workflow';
import {
validateJSON,
zendeskApiRequest,
zendeskApiRequestAllItems,
} from './GenericFunctions';
@ -30,6 +31,7 @@ import {
ITicket,
IComment,
} from './TicketInterface';
import { response } from 'express';
export class Zendesk implements INodeType {
description: INodeTypeDescription = {
@ -83,6 +85,33 @@ export class Zendesk implements INodeType {
methods = {
loadOptions: {
// Get all the custom fields to display them to user so that he can
// select them easily
async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const customFields = [
'text',
'textarea',
'date',
'integer',
'decimal',
'regexp',
'multiselect',
'tagger',
];
const fields = await zendeskApiRequestAllItems.call(this, 'ticket_fields', 'GET', '/ticket_fields');
for (const field of fields) {
if (customFields.includes(field.type)) {
const fieldName = field.title;
const fieldId = field.id;
returnData.push({
name: fieldName,
value: fieldId,
});
}
}
return returnData;
},
// Get all the groups to display them to user so that he can
// select them easily
async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
@ -131,42 +160,54 @@ export class Zendesk implements INodeType {
if (operation === 'create') {
const description = this.getNodeParameter('description', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const comment: IComment = {
body: description,
};
const body: ITicket = {
comment,
};
if (additionalFields.type) {
body.type = additionalFields.type as string;
}
if (additionalFields.externalId) {
body.external_id = additionalFields.externalId as string;
}
if (additionalFields.subject) {
body.subject = additionalFields.subject as string;
}
if (additionalFields.status) {
body.status = additionalFields.status as string;
}
if (additionalFields.recipient) {
body.recipient = additionalFields.recipient as string;
}
if (additionalFields.group) {
body.group = additionalFields.group as string;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string[];
}
if (jsonParameters) {
const customFieldsJson = this.getNodeParameter('customFieldsJson', i) as string;
try {
JSON.parse(customFieldsJson);
} catch(err) {
throw new Error('Custom fields must be a valid JSON');
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '' ) {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.type) {
body.type = additionalFields.type as string;
}
if (additionalFields.externalId) {
body.external_id = additionalFields.externalId as string;
}
if (additionalFields.subject) {
body.subject = additionalFields.subject as string;
}
if (additionalFields.status) {
body.status = additionalFields.status as string;
}
if (additionalFields.recipient) {
body.recipient = additionalFields.recipient as string;
}
if (additionalFields.group) {
body.group = additionalFields.group as string;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string[];
}
if (additionalFields.customFieldsUi) {
body.custom_fields = (additionalFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
}
body.custom_fields = JSON.parse(customFieldsJson);
}
responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body });
responseData = responseData.ticket;
@ -175,37 +216,50 @@ export class Zendesk implements INodeType {
if (operation === 'update') {
const ticketId = this.getNodeParameter('id', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: ITicket = {};
if (updateFields.type) {
body.type = updateFields.type as string;
}
if (updateFields.externalId) {
body.external_id = updateFields.externalId as string;
}
if (updateFields.subject) {
body.subject = updateFields.subject as string;
}
if (updateFields.status) {
body.status = updateFields.status as string;
}
if (updateFields.recipient) {
body.recipient = updateFields.recipient as string;
}
if (updateFields.group) {
body.group = updateFields.group as string;
}
if (updateFields.tags) {
body.tags = updateFields.tags as string[];
}
if (jsonParameters) {
const customFieldsJson = this.getNodeParameter('customFieldsJson', i) as string;
try {
JSON.parse(customFieldsJson);
} catch(err) {
throw new Error('Custom fields must be a valid JSON');
const updateFieldsJson = this.getNodeParameter('updateFieldsJson', i) as string;
if (updateFieldsJson !== '' ) {
if (validateJSON(updateFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(updateFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
if (updateFields.type) {
body.type = updateFields.type as string;
}
if (updateFields.externalId) {
body.external_id = updateFields.externalId as string;
}
if (updateFields.subject) {
body.subject = updateFields.subject as string;
}
if (updateFields.status) {
body.status = updateFields.status as string;
}
if (updateFields.recipient) {
body.recipient = updateFields.recipient as string;
}
if (updateFields.group) {
body.group = updateFields.group as string;
}
if (updateFields.tags) {
body.tags = updateFields.tags as string[];
}
if (updateFields.customFieldsUi) {
body.custom_fields = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
}
body.custom_fields = JSON.parse(customFieldsJson);
}
responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body });
responseData = responseData.ticket;
@ -259,8 +313,15 @@ export class Zendesk implements INodeType {
}
//https://developer.zendesk.com/rest_api/docs/support/ticket_fields#list-ticket-fields
if (operation === 'getAll') {
responseData = await zendeskApiRequest.call(this, 'GET', '/ticket_fields', {}, qs);
responseData = responseData.ticket_fields;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(this, 'ticket_fields', 'GET', '/ticket_fields', {}, qs);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
responseData = await zendeskApiRequestAllItems.call(this, 'ticket_fields', 'GET', '/ticket_fields', {}, qs);
responseData = responseData.slice(0, limit);
}
}
}
if (Array.isArray(responseData)) {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.55.0",
"version": "0.57.1",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -287,7 +287,7 @@
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^24.9.0",
"n8n-workflow": "~0.26.0",
"n8n-workflow": "~0.28.0",
"ts-jest": "^24.0.2",
"tslint": "^5.17.0",
"typescript": "~3.7.4"
@ -310,7 +310,7 @@
"moment-timezone": "0.5.28",
"mongodb": "^3.3.2",
"mysql2": "^2.0.1",
"n8n-core": "~0.29.0",
"n8n-core": "~0.31.0",
"nodemailer": "^5.1.1",
"pdf-parse": "^1.1.1",
"pg-promise": "^9.0.3",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.26.0",
"version": "0.28.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View file

@ -206,7 +206,7 @@ export interface IExecuteFunctions {
getWorkflowStaticData(type: string): IDataObject;
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@ -225,7 +225,7 @@ export interface IExecuteSingleFunctions {
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflow(): IWorkflowMetadata;
getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
@ -260,7 +260,7 @@ export interface IHookFunctions {
getTimezone(): string;
getWebhookDescription(name: string): IWebhookDescription | undefined;
getWebhookName(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@ -275,7 +275,7 @@ export interface IPollFunctions {
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@ -290,7 +290,7 @@ export interface ITriggerFunctions {
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflow(): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@ -311,7 +311,7 @@ export interface IWebhookFunctions {
getTimezone(): string;
getWebhookName(): string;
getWorkflowStaticData(type: string): IDataObject;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@ -331,6 +331,7 @@ export interface INode {
retryOnFail?: boolean;
maxTries?: number;
waitBetweenTries?: number;
alwaysOutputData?: boolean;
continueOnFail?: boolean;
parameters: INodeParameters;
credentials?: INodeCredentials;
@ -573,6 +574,7 @@ export interface IWorkflowDataProxyData {
$env: any; // tslint:disable-line:no-any
$evaluateExpression: any; // tslint:disable-line:no-any
$item: any; // tslint:disable-line:no-any
$items: any; // tslint:disable-line:no-any
$json: any; // tslint:disable-line:no-any
$node: any; // tslint:disable-line:no-any
$parameter: any; // tslint:disable-line:no-any

View file

@ -899,14 +899,13 @@ export class Workflow {
// Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(this, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData);
const data = dataProxy.getDataProxy();
data.$evaluateExpression = (expression: string) => {
return this.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString);
};
// Execute the expression
try {
const returnValue = tmpl.tmpl(parameterValue, data);
if (returnValue !== null && typeof returnValue === 'object') {
if (typeof returnValue === 'function') {
throw new Error('Expression resolved to a function. Please add "()"');
} else if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString === true) {
return this.convertObjectValueToString(returnValue);
}

View file

@ -12,6 +12,7 @@ import {
export class WorkflowDataProxy {
private workflow: Workflow;
private runExecutionData: IRunExecutionData | null;
private defaultReturnRunIndex: number;
private runIndex: number;
private itemIndex: number;
private activeNodeName: string;
@ -19,9 +20,10 @@ export class WorkflowDataProxy {
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[]) {
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], defaultReturnRunIndex = -1) {
this.workflow = workflow;
this.runExecutionData = runExecutionData;
this.defaultReturnRunIndex = defaultReturnRunIndex;
this.runIndex = runIndex;
this.itemIndex = itemIndex;
this.activeNodeName = activeNodeName;
@ -104,6 +106,80 @@ export class WorkflowDataProxy {
}
/**
* Returns the node ExecutionData
*
* @private
* @param {string} nodeName The name of the node query data from
* @param {boolean} [shortSyntax=false] If short syntax got used
* @param {number} [outputIndex] The index of the output, if not given the first one gets used
* @param {number} [runIndex] The index of the run, if not given the current one does get used
* @returns {INodeExecutionData[]}
* @memberof WorkflowDataProxy
*/
private getNodeExecutionData(nodeName: string, shortSyntax = false, outputIndex?: number, runIndex?: number): INodeExecutionData[] {
const that = this;
let executionData: INodeExecutionData[];
if (shortSyntax === false) {
// Long syntax got used to return data from node in path
if (that.runExecutionData === null) {
throw new Error(`Workflow did not run so do not have any execution-data.`);
}
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
throw new Error(`No execution data found for node "${nodeName}"`);
}
runIndex = runIndex === undefined ? that.defaultReturnRunIndex : runIndex;
runIndex = runIndex === -1 ? (that.runExecutionData.resultData.runData[nodeName].length -1) : runIndex;
if (that.runExecutionData.resultData.runData[nodeName].length < runIndex) {
throw new Error(`No execution data found for run "${runIndex}" of node "${nodeName}"`);
}
const taskData = that.runExecutionData.resultData.runData[nodeName][runIndex].data!;
if (taskData.main === null || !taskData.main.length || taskData.main[0] === null) {
// throw new Error(`No data found for item-index: "${itemIndex}"`);
throw new Error(`No data found from "main" input.`);
}
// Check from which output to read the data.
// Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output)
if (outputIndex === undefined) {
const outputIndex = that.workflow.getNodeConnectionOutputIndex(that.activeNodeName, nodeName, 'main');
if (outputIndex === undefined) {
throw new Error(`The node "${that.activeNodeName}" is not connected with node "${nodeName}" so no data can get returned from it.`);
}
}
if (outputIndex === undefined) {
outputIndex = 0;
}
if (taskData.main.length < outputIndex) {
throw new Error(`No data found from "main" input with index "${outputIndex}" via which node is connected with.`);
}
executionData = taskData.main[outputIndex] as INodeExecutionData[];
} else {
// Short syntax got used to return data from active node
// TODO: Here have to generate connection Input data for the current node by itself
// Data needed:
// #- the run-index
// - node which did send data (has to be the one from last recent execution)
// - later also the name of the input and its index (currently not needed as it is always "main" and index "0")
executionData = that.connectionInputData;
}
return executionData;
}
/**
* Returns a proxy which allows to query data of a given node
@ -128,53 +204,7 @@ export class WorkflowDataProxy {
name = name.toString();
if (['binary', 'data', 'json'].includes(name)) {
let executionData: INodeExecutionData[];
if (shortSyntax === false) {
// Long syntax got used to return data from node in path
if (that.runExecutionData === null) {
throw new Error(`Workflow did not run so do not have any execution-data.`);
}
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
throw new Error(`No execution data found for node "${nodeName}"`);
}
if (that.runExecutionData.resultData.runData[nodeName].length < that.runIndex) {
throw new Error(`No execution data found for run "${that.runIndex}" of node "${nodeName}"`);
}
const taskData = that.runExecutionData.resultData.runData[nodeName][that.runIndex].data!;
if (taskData.main === null || !taskData.main.length || taskData.main[0] === null) {
// throw new Error(`No data found for item-index: "${itemIndex}"`);
throw new Error(`No data found from "main" input.`);
}
// Check from which output to read the data.
// Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output)
const outputIndex = that.workflow.getNodeConnectionOutputIndex(that.activeNodeName, nodeName, 'main');
if (outputIndex === undefined) {
throw new Error(`The node "${that.activeNodeName}" is not connected with node "${nodeName}" so no data can get returned from it.`);
}
if (taskData.main.length < outputIndex) {
throw new Error(`No data found from "main" input with index "${outputIndex}" via which node is connected with.`);
}
executionData = taskData.main[outputIndex] as INodeExecutionData[];
} else {
// Short syntax got used to return data from active node
// TODO: Here have to generate connection Input data for the current node by itself
// Data needed:
// #- the run-index
// - node which did send data (has to be the one from last recent execution)
// - later also the name of the input and its index (currently not needed as it is always "main" and index "0")
executionData = that.connectionInputData;
}
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
if (executionData.length <= that.itemIndex) {
throw new Error(`No data found for item-index: "${that.itemIndex}"`);
@ -214,6 +244,11 @@ export class WorkflowDataProxy {
} else if (name === 'parameter') {
// Get node parameter data
return that.nodeParameterGetter(nodeName);
} else if (name === 'runIndex') {
if (that.runExecutionData === null || !that.runExecutionData.resultData.runData[nodeName]) {
return -1;
}
return that.runExecutionData.resultData.runData[nodeName].length - 1;
}
return Reflect.get(target, name, receiver);
@ -300,14 +335,32 @@ export class WorkflowDataProxy {
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
$evaluateExpression: (expression: string) => { }, // Placeholder
$item: (itemIndex: number) => {
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData);
$evaluateExpression: (expression: string, itemIndex?: number) => {
itemIndex = itemIndex || that.itemIndex;
return that.workflow.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData);
},
$item: (itemIndex: number, runIndex?: number) => {
const defaultReturnRunIndex = runIndex === undefined ? -1 : runIndex;
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, defaultReturnRunIndex);
return dataProxy.getDataProxy();
},
$items: (nodeName?: string, outputIndex?: number, runIndex?: number) => {
let executionData: INodeExecutionData[];
if (nodeName === undefined) {
executionData = that.connectionInputData;
} else {
outputIndex = outputIndex || 0;
runIndex = runIndex === undefined ? -1 : runIndex;
executionData = that.getNodeExecutionData(nodeName, false, outputIndex, runIndex);
}
return executionData;
},
$json: {}, // Placeholder
$node: this.nodeGetter(),
$parameter: this.nodeParameterGetter(this.activeNodeName),
$runIndex: this.runIndex,
$workflow: this.workflowGetter(),
};

View file

@ -936,6 +936,29 @@ describe('Workflow', () => {
value1: 'default-value1',
},
},
{
description: 'return resolved value when referencing another property with expression on another node (long "$node["{NODE}"].parameter" syntax)',
input: {
Node1: {
parameters: {
value1: 'valueNode1',
}
},
Node2: {
parameters: {
value1: '={{$node["Node1"].parameter.value1}}a',
},
},
Node3: {
parameters: {
value1: '={{$node["Node2"].parameter.value1}}b',
},
}
},
output: {
value1: 'valueNode1ab',
},
},
// TODO: Make that this test does not fail!
// {
// description: 'return resolved value when short "data" syntax got used in expression on paramter of not active node which got referenced by active one',
@ -1203,11 +1226,12 @@ describe('Workflow', () => {
{
startTime: 1,
executionTime: 1,
// @ts-ignore
data: {
main: [
[
{}
{
json: {},
}
]
]
}