diff --git a/docs/database.md b/docs/database.md index 8fa4325394..8daf5970ce 100644 --- a/docs/database.md +++ b/docs/database.md @@ -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) diff --git a/docs/node-basics.md b/docs/node-basics.md index dae607fb82..f81fb6fcee 100644 --- a/docs/node-basics.md +++ b/docs/node-basics.md @@ -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. diff --git a/docs/nodes.md b/docs/nodes.md index a3b0fc7b2e..8c3b061474 100644 --- a/docs/nodes.md +++ b/docs/nodes.md @@ -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; ``` diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index cedccc2704..ff191b5eb8 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -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? diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index adfc2d7e44..6148797615 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -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' }, diff --git a/packages/cli/package.json b/packages/cli/package.json index fc8436df82..bc55200fd5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index a97cbaa837..ed24baabd2 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -63,11 +63,12 @@ export async function init(synchronize?: boolean): Promise }; 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, diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index de81711338..e72ce7839d 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -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 { diff --git a/packages/core/package.json b/packages/core/package.json index 4465fd911f..36417f38f6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" }, diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 1dfb599083..1e8b8898a7 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -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 diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 00d1fc38a8..9560f3377a 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -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", diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 98c4069e5c..515eeccce5 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -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,
it returns an empty item anyway. Be careful setting this on
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); diff --git a/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts b/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts index bac4b2213f..efd233f749 100644 --- a/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts +++ b/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts @@ -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 { @@ -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); } diff --git a/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts b/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts index e448ce670e..92420c2570 100644 --- a/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts +++ b/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts @@ -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 Mongolite format.', }, { 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 Mongolite format.', }, ], }, @@ -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[]; diff --git a/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts b/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts index 4a2c640f84..d508629840 100644 --- a/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts +++ b/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts @@ -9,7 +9,7 @@ import { cockpitApiRequest } from './GenericFunctions'; export async function createCollectionEntry(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, data: IDataObject, id?: string): Promise { // 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) { diff --git a/packages/nodes-base/nodes/Cockpit/FormDescription.ts b/packages/nodes-base/nodes/Cockpit/FormDescription.ts index 8488cbe095..66a5c052ec 100644 --- a/packages/nodes-base/nodes/Cockpit/FormDescription.ts +++ b/packages/nodes-base/nodes/Cockpit/FormDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/Cockpit/FormFunctions.ts b/packages/nodes-base/nodes/Cockpit/FormFunctions.ts index 437ed210a0..36a1bdc389 100644 --- a/packages/nodes-base/nodes/Cockpit/FormFunctions.ts +++ b/packages/nodes-base/nodes/Cockpit/FormFunctions.ts @@ -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); diff --git a/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts b/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts index ed923d3bda..3f3bf39ac2 100644 --- a/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts @@ -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; +} diff --git a/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts b/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts index e9774b4f5f..98df8a03d9 100644 --- a/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts +++ b/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts @@ -16,7 +16,7 @@ export const singletonOperations = [ { name: 'Get', value: 'get', - description: 'Gets a singleton', + description: 'Gets a Singleton', }, ], default: 'get', diff --git a/packages/nodes-base/nodes/Function.node.ts b/packages/nodes-base/nodes/Function.node.ts index 253a216650..7f5ea4c1ea 100644 --- a/packages/nodes-base/nodes/Function.node.ts +++ b/packages/nodes-base/nodes/Function.node.ts @@ -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, diff --git a/packages/nodes-base/nodes/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem.node.ts index 356e419477..6352fc3f98 100644 --- a/packages/nodes-base/nodes/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem.node.ts @@ -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; }, diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 5d7273cec2..990c007e07 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -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') { diff --git a/packages/nodes-base/nodes/Mattermost/GenericFunctions.ts b/packages/nodes-base/nodes/Mattermost/GenericFunctions.ts index bb08acdf76..329e3ae83b 100644 --- a/packages/nodes-base/nodes/Mattermost/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mattermost/GenericFunctions.ts @@ -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 * diff --git a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts index 5026955952..e9f81e7d8d 100644 --- a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts +++ b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts @@ -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 here', + 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, diff --git a/packages/nodes-base/nodes/SpreadsheetFile.node.ts b/packages/nodes-base/nodes/SpreadsheetFile.node.ts index ecac7db2cd..594754ef99 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile.node.ts @@ -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 diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index f098f97358..ffe371e160 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -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; +} diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts index 7642248e7e..3bb3d41417 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -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 Details`, + description: `Object of values to set as described here.`, }, + /* -------------------------------------------------------------------------- */ /* 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 Details`, + description: `Object of values to update as described here.`, }, + /* -------------------------------------------------------------------------- */ /* 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: '', diff --git a/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts index 8e061db402..9ba754eef4 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketFieldDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index b66481bcc1..8a2586d8d8 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -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 { + 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 { @@ -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)) { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dbf0ac449f..a6a5e9eb55 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -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", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index d3a56dc2b2..24a0481c8f 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -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", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 443b72d32c..5b4bd7dcde 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -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; 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; 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 diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 816c009a1f..6c1c48ad56 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -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); } diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 527b186196..8094991f3c 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -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(), }; diff --git a/packages/workflow/test/Workflow.test.ts b/packages/workflow/test/Workflow.test.ts index 09cc1b871a..51b092bef7 100644 --- a/packages/workflow/test/Workflow.test.ts +++ b/packages/workflow/test/Workflow.test.ts @@ -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: {}, + } ] ] }