diff --git a/docker/images/n8n-custom/docker-entrypoint.sh b/docker/images/n8n-custom/docker-entrypoint.sh index 2dd4dae105..acd6a6019c 100755 --- a/docker/images/n8n-custom/docker-entrypoint.sh +++ b/docker/images/n8n-custom/docker-entrypoint.sh @@ -6,6 +6,8 @@ if [ -d /root/.n8n ] ; then ln -s /root/.n8n /home/node/ fi +chown -R node /home/node + if [ "$#" -gt 0 ]; then # Got started with arguments COMMAND=$1; diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index b6641628df..b74eb7397d 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -11,6 +11,7 @@ import { CredentialTypes, Db, ExternalHooks, + InternalHooksManager, IWorkflowBase, IWorkflowExecutionDataProcess, LoadNodesAndCredentials, @@ -123,6 +124,9 @@ export class Execute extends Command { const externalHooks = ExternalHooks(); await externalHooks.init(); + const instanceId = await UserSettings.getInstanceId(); + InternalHooksManager.init(instanceId); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/commands/executeBatch.ts b/packages/cli/commands/executeBatch.ts index d4489c38d4..587f91c2a9 100644 --- a/packages/cli/commands/executeBatch.ts +++ b/packages/cli/commands/executeBatch.ts @@ -28,6 +28,7 @@ import { CredentialTypes, Db, ExternalHooks, + InternalHooksManager, IWorkflowDb, IWorkflowExecutionDataProcess, LoadNodesAndCredentials, @@ -55,12 +56,12 @@ export class ExecuteBatch extends Command { static executionTimeout = 3 * 60 * 1000; static examples = [ - `$ n8n executeAll`, - `$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`, - `$ n8n executeAll --debug --output=/data/output.json`, - `$ n8n executeAll --ids=10,13,15 --shortOutput`, - `$ n8n executeAll --snapshot=/data/snapshots --shallow`, - `$ n8n executeAll --compare=/data/previousExecutionData --retries=2`, + `$ n8n executeBatch`, + `$ n8n executeBatch --concurrency=10 --skipList=/data/skipList.txt`, + `$ n8n executeBatch --debug --output=/data/output.json`, + `$ n8n executeBatch --ids=10,13,15 --shortOutput`, + `$ n8n executeBatch --snapshot=/data/snapshots --shallow`, + `$ n8n executeBatch --compare=/data/previousExecutionData --retries=2`, ]; static flags = { @@ -303,6 +304,9 @@ export class ExecuteBatch extends Command { const externalHooks = ExternalHooks(); await externalHooks.init(); + const instanceId = await UserSettings.getInstanceId(); + InternalHooksManager.init(instanceId); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); @@ -813,10 +817,22 @@ export class ExecuteBatch extends Command { const changes = diff(JSON.parse(contents), data, { keysOnly: true }); if (changes !== undefined) { - // we have structural changes. Report them. - executionResult.error = `Workflow may contain breaking changes`; - executionResult.changes = changes; - executionResult.executionStatus = 'error'; + // If we had only additions with no removals + // Then we treat as a warning and not an error. + // To find this, we convert the object to JSON + // and search for the `__deleted` string + const changesJson = JSON.stringify(changes); + if (changesJson.includes('__deleted')) { + // we have structural changes. Report them. + executionResult.error = 'Workflow may contain breaking changes'; + executionResult.changes = changes; + executionResult.executionStatus = 'error'; + } else { + executionResult.error = + 'Workflow contains new data that previously did not exist.'; + executionResult.changes = changes; + executionResult.executionStatus = 'warning'; + } } else { executionResult.executionStatus = 'success'; } diff --git a/packages/cli/package.json b/packages/cli/package.json index 29cee52fde..070ab83f9f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.148.0", + "version": "0.149.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -110,10 +110,10 @@ "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", "mysql2": "~2.3.0", - "n8n-core": "~0.92.0", - "n8n-editor-ui": "~0.115.0", - "n8n-nodes-base": "~0.145.0", - "n8n-workflow": "~0.75.0", + "n8n-core": "~0.93.0", + "n8n-editor-ui": "~0.116.0", + "n8n-nodes-base": "~0.146.0", + "n8n-workflow": "~0.76.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 69c6dcb527..67c5f406c3 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1596,11 +1596,11 @@ class App { const findQuery = {} as FindManyOptions; if (req.query.filter) { findQuery.where = JSON.parse(req.query.filter as string); - if ((findQuery.where! as IDataObject).id !== undefined) { + if (findQuery.where.id !== undefined) { // No idea if multiple where parameters make db search // slower but to be sure that that is not the case we // remove all unnecessary fields in case the id is defined. - findQuery.where = { id: (findQuery.where! as IDataObject).id }; + findQuery.where = { id: findQuery.where.id }; } } diff --git a/packages/core/package.json b/packages/core/package.json index 9ac21f0dec..dfc9c18a51 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.92.0", + "version": "0.93.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -50,7 +50,7 @@ "form-data": "^4.0.0", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.75.0", + "n8n-workflow": "~0.76.0", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "qs": "^6.10.1", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 29b0c140e7..fa1894a1da 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -87,6 +87,8 @@ import { axios.defaults.timeout = 300000; // Prevent axios from adding x-form-www-urlencoded headers by default axios.defaults.headers.post = {}; +axios.defaults.headers.put = {}; +axios.defaults.headers.patch = {}; axios.defaults.paramsSerializer = (params) => { if (params instanceof URLSearchParams) { return params.toString(); @@ -135,6 +137,28 @@ function searchForHeader(headers: IDataObject, headerName: string) { return headerNames.find((thisHeader) => thisHeader.toLowerCase() === headerName); } +async function generateContentLengthHeader(formData: FormData, headers: IDataObject) { + if (!formData || !formData.getLength) { + return; + } + try { + const length = await new Promise((res, rej) => { + formData.getLength((error: Error | null, length: number) => { + if (error) { + rej(error); + return; + } + res(length); + }); + }); + headers = Object.assign(headers, { + 'content-length': length, + }); + } catch (error) { + Logger.error('Unable to calculate form data length', { error }); + } +} + async function parseRequestObject(requestObject: IDataObject) { // This function is a temporary implementation // That translates all http requests done via @@ -199,6 +223,7 @@ async function parseRequestObject(requestObject: IDataObject) { delete axiosConfig.headers[contentTypeHeaderKeyName]; const headers = axiosConfig.data.getHeaders(); axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers); + await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers); } else { // When using the `form` property it means the content should be x-www-form-urlencoded. if (requestObject.form !== undefined && requestObject.body === undefined) { @@ -235,6 +260,7 @@ async function parseRequestObject(requestObject: IDataObject) { // Mix in headers as FormData creates the boundary. const headers = axiosConfig.data.getHeaders(); axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers); + await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers); } else if (requestObject.body !== undefined) { // If we have body and possibly form if (requestObject.form !== undefined) { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index e4b72499d5..3a4efd3121 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.115.0", + "version": "0.116.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -72,7 +72,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.75.0", + "n8n-workflow": "~0.76.0", "sass": "^1.26.5", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index f5d167ae74..224f012bf3 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1686,7 +1686,9 @@ export default mixins( await this.$store.dispatch('workflows/setNewWorkflowName'); this.$store.commit('setStateDirty', false); - await this.addNodes([DEFAULT_START_NODE]); + const nodes = [{...DEFAULT_START_NODE}]; + + await this.addNodes(nodes); this.$store.commit('setStateDirty', false); this.setZoomLevel(1); diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index baed211bb4..3c2de68f43 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "0.32.0", + "version": "0.33.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -60,8 +60,8 @@ "change-case": "^4.1.1", "copyfiles": "^2.1.1", "inquirer": "^7.0.1", - "n8n-core": "~0.92.0", - "n8n-workflow": "~0.75.0", + "n8n-core": "~0.93.0", + "n8n-workflow": "~0.76.0", "oauth-1.0a": "^2.2.6", "replace-in-file": "^6.0.0", "request": "^2.88.2", diff --git a/packages/nodes-base/credentials/OneSimpleApi.credentials.ts b/packages/nodes-base/credentials/OneSimpleApi.credentials.ts new file mode 100644 index 0000000000..63ec3d13a9 --- /dev/null +++ b/packages/nodes-base/credentials/OneSimpleApi.credentials.ts @@ -0,0 +1,19 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + + +export class OneSimpleApi implements ICredentialType { + name = 'oneSimpleApi'; + displayName = 'One Simple API'; + documentationUrl = 'oneSimpleApi'; + properties: INodeProperties[] = [ + { + displayName: 'API Token', + name: 'apiToken', + type: 'string', + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.json b/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.json new file mode 100644 index 0000000000..4b47e8e54e --- /dev/null +++ b/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.dropcontact", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Sales" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/dropcontact" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.dropcontact/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index a499cabeb1..ec34be54fa 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -152,6 +152,10 @@ const nodeOperationOptions: INodeProperties[] = [ }, }, options: [ + { + name: 'Circle', + value: 'circle', + }, { name: 'Line', value: 'line', @@ -192,6 +196,7 @@ const nodeOperationOptions: INodeProperties[] = [ 'draw', ], primitive: [ + 'circle', 'line', 'rectangle', ], @@ -210,6 +215,7 @@ const nodeOperationOptions: INodeProperties[] = [ 'draw', ], primitive: [ + 'circle', 'line', 'rectangle', ], @@ -228,6 +234,7 @@ const nodeOperationOptions: INodeProperties[] = [ 'draw', ], primitive: [ + 'circle', 'line', 'rectangle', ], @@ -246,6 +253,7 @@ const nodeOperationOptions: INodeProperties[] = [ 'draw', ], primitive: [ + 'circle', 'line', 'rectangle', ], @@ -472,6 +480,110 @@ const nodeOperationOptions: INodeProperties[] = [ }, description: 'The name of the binary property which contains the data of the image to composite on top of image which is found in Property Name.', }, + { + displayName: 'Operator', + name: 'operator', + type: 'options', + displayOptions: { + show: { + operation: [ + 'composite', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'Add', + }, + { + name: 'Atop', + value: 'Atop', + }, + { + name: 'Bumpmap', + value: 'Bumpmap', + }, + { + name: 'Copy', + value: 'Copy', + }, + { + name: 'Copy Black', + value: 'CopyBlack', + }, + { + name: 'Copy Blue', + value: 'CopyBlue', + }, + { + name: 'Copy Cyan', + value: 'CopyCyan', + }, + { + name: 'Copy Green', + value: 'CopyGreen', + }, + { + name: 'Copy Magenta', + value: 'CopyMagenta', + }, + { + name: 'Copy Opacity', + value: 'CopyOpacity', + }, + { + name: 'Copy Red', + value: 'CopyRed', + }, + { + name: 'Copy Yellow', + value: 'CopyYellow', + }, + { + name: 'Difference', + value: 'Difference', + }, + { + name: 'Divide', + value: 'Divide', + }, + { + name: 'In', + value: 'In', + }, + { + name: 'Minus', + value: 'Minus', + }, + { + name: 'Multiply', + value: 'Multiply', + }, + { + name: 'Out', + value: 'Out', + }, + { + name: 'Over', + value: 'Over', + }, + { + name: 'Plus', + value: 'Plus', + }, + { + name: 'Subtract', + value: 'Subtract', + }, + { + name: 'Xor', + value: 'Xor', + }, + ], + default: 'Over', + description: 'The operator to use to combine the images.', + }, { displayName: 'Position X', name: 'positionX', @@ -1095,6 +1207,7 @@ export class EditImage implements INodeType { } else if (operationData.operation === 'composite') { const positionX = operationData.positionX as number; const positionY = operationData.positionY as number; + const operator = operationData.operator as string; const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; @@ -1109,9 +1222,9 @@ export class EditImage implements INodeType { if (operations[0].operation === 'create') { // It seems like if the image gets created newly we have to create a new gm instance // else it fails for some reason - gmInstance = gm(gmInstance!.stream('png')).geometry(geometryString).composite(path); + gmInstance = gm(gmInstance!.stream('png')).compose(operator).geometry(geometryString).composite(path); } else { - gmInstance = gmInstance!.geometry(geometryString).composite(path); + gmInstance = gmInstance!.compose(operator).geometry(geometryString).composite(path); } if (operations.length !== i + 1) { @@ -1131,6 +1244,8 @@ export class EditImage implements INodeType { if (operationData.primitive === 'line') { gmInstance = gmInstance.drawLine(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number); + } else if (operationData.primitive === 'circle') { + gmInstance = gmInstance.drawCircle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number); } else if (operationData.primitive === 'rectangle') { gmInstance = gmInstance.drawRectangle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number, operationData.cornerRadius as number || undefined); } diff --git a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts index f956251911..37b02c9343 100644 --- a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts +++ b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts @@ -124,7 +124,7 @@ export class GoogleTasks implements INodeType { body.notes = additionalFields.notes as string; } if (additionalFields.dueDate) { - body.dueDate = additionalFields.dueDate as string; + body.due = additionalFields.dueDate as string; } if (additionalFields.completed) { @@ -249,7 +249,7 @@ export class GoogleTasks implements INodeType { } if (updateFields.dueDate) { - body.dueDate = updateFields.dueDate as string; + body.due = updateFields.dueDate as string; } if (updateFields.completed) { diff --git a/packages/nodes-base/nodes/Google/Task/TaskDescription.ts b/packages/nodes-base/nodes/Google/Task/TaskDescription.ts index aaf2892ab5..90747569d6 100644 --- a/packages/nodes-base/nodes/Google/Task/TaskDescription.ts +++ b/packages/nodes-base/nodes/Google/Task/TaskDescription.ts @@ -447,6 +447,13 @@ export const taskFields = [ default: false, description: 'Flag indicating whether the task has been deleted.', }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + description: 'Due date of the task.', + }, { displayName: 'Notes', name: 'notes', diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index 3cd0509566..579840d3a9 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -179,7 +179,7 @@ export class Jira implements INodeType { } catch (err) { return { status: 'Error', - message: `Connection details not valid; ${err.message}`, + message: `Connection details not valid: ${err.message}`, }; } return { diff --git a/packages/nodes-base/nodes/OneSimpleApi/GenericFunctions.ts b/packages/nodes-base/nodes/OneSimpleApi/GenericFunctions.ts new file mode 100644 index 0000000000..5e89c03156 --- /dev/null +++ b/packages/nodes-base/nodes/OneSimpleApi/GenericFunctions.ts @@ -0,0 +1,41 @@ +import { + OptionsWithUri +} from 'request'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +export async function oneSimpleApiRequest(this: IExecuteFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) { + const credentials = await this.getCredentials('oneSimpleApi'); + if (credentials === undefined) { + throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); + } + + const outputFormat = 'json'; + let options: OptionsWithUri = { + method, + body, + qs, + uri: uri || `https://onesimpleapi.com/api${resource}?token=${credentials.apiToken}&output=${outputFormat}`, + json: true, + }; + options = Object.assign({}, options, option); + + if (Object.keys(body).length === 0) { + delete options.body; + } + + try { + const responseData = await this.helpers.request(options); + return responseData; + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} diff --git a/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.json b/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.json new file mode 100644 index 0000000000..bd85e8ee3b --- /dev/null +++ b/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.oneSimpleApi", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Utility" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/OneSimpleAPI" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.oneSimpleApi/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts b/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts new file mode 100644 index 0000000000..5edda2ee4b --- /dev/null +++ b/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts @@ -0,0 +1,867 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + oneSimpleApiRequest, +} from './GenericFunctions'; + +export class OneSimpleApi implements INodeType { + description: INodeTypeDescription = { + displayName: 'One Simple API', + name: 'oneSimpleApi', + icon: 'file:onesimpleapi.svg', + group: ['transform'], + version: 1, + description: 'A toolbox of no-code utilities', + defaults: { + name: 'One Simple API', + color: '#1A82e2', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'oneSimpleApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Information', + value: 'information', + }, + { + name: 'Utility', + value: 'utility', + }, + { + name: 'Website', + value: 'website', + }, + ], + default: 'website', + required: true, + }, + // Generation + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'website', + ], + }, + }, + options: [ + { + name: 'Generate PDF', + value: 'pdf', + description: 'Generate a PDF from a webpage', + }, + { + name: 'Get SEO Data', + value: 'seo', + description: 'Get SEO information from website', + }, + { + name: 'Take Screenshot', + value: 'screenshot', + description: 'Create a screenshot from a webpage', + }, + ], + default: 'pdf', + }, + // Information + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'information', + ], + }, + }, + options: [ + { + name: 'Exchange Rate', + value: 'exchangeRate', + description: 'Convert a value between currencies', + }, + { + name: 'Image Metadata', + value: 'imageMetadata', + description: 'Retrieve image metadata from a URL', + }, + ], + default: 'exchangeRate', + description: 'The operation to perform.', + }, + // Utiliy + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'utility', + ], + }, + }, + options: [ + { + name: 'Expand URL', + value: 'expandURL', + description: 'Expand a shortened url', + }, + { + name: 'Generate QR Code', + value: 'qrCode', + description: 'Generate a QR Code', + }, + { + name: 'Validate Email', + value: 'validateEmail', + description: 'Validate an email address', + }, + ], + default: 'validateEmail', + description: 'The operation to perform.', + }, + // website: pdf + { + displayName: 'Webpage URL', + name: 'link', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'pdf', + ], + resource: [ + 'website', + ], + }, + }, + default: '', + description: 'Link to webpage to convert', + }, + { + displayName: 'Download PDF?', + name: 'download', + type: 'boolean', + required: true, + displayOptions: { + show: { + operation: [ + 'pdf', + ], + resource: [ + 'website', + ], + }, + }, + default: false, + description: 'Whether to download the PDF or return a link to it', + }, + { + displayName: 'Put Output In Field', + name: 'output', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'pdf', + ], + resource: [ + 'website', + ], + download: [ + true, + ], + }, + }, + default: 'data', + description: 'The name of the output field to put the binary file data in', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'website', + ], + operation: [ + 'pdf', + ], + }, + }, + options: [ + { + displayName: 'Page Size', + name: 'page', + type: 'options', + options: [ + { + name: 'A0', + value: 'A0', + }, + { + name: 'A1', + value: 'A1', + }, + { + name: 'A2', + value: 'A2', + }, + { + name: 'A3', + value: 'A3', + }, + { + name: 'A4', + value: 'A4', + }, + { + name: 'A5', + value: 'A5', + }, + { + name: 'A6', + value: 'A6', + }, + { + name: 'Legal', + value: 'Legal', + }, + { + name: 'Ledger', + value: 'Ledger', + }, + { + name: 'Letter', + value: 'Letter', + }, + { + name: 'Tabloid', + value: 'Tabloid', + }, + ], + default: '', + description: 'The page size', + }, + { + displayName: 'Force Refresh', + name: 'force', + type: 'boolean', + default: false, + description: `Normally the API will reuse a previously taken screenshot of the URL to give a faster response. + This option allows you to retake the screenshot at that exact time, for those times when it's necessary`, + }, + ], + }, + // website: qrCode + { + displayName: 'QR Content', + name: 'message', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'qrCode', + ], + resource: [ + 'utility', + ], + }, + }, + default: '', + description: 'The text that should be turned into a QR code - like a website URL', + }, + { + displayName: 'Download Image?', + name: 'download', + type: 'boolean', + required: true, + displayOptions: { + show: { + operation: [ + 'qrCode', + ], + resource: [ + 'utility', + ], + }, + }, + default: false, + description: 'Whether to download the QR code or return a link to it', + }, + { + displayName: 'Put Output In Field', + name: 'output', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'qrCode', + ], + resource: [ + 'utility', + ], + download: [ + true, + ], + }, + }, + default: 'data', + description: 'The name of the output field to put the binary file data in', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'utility', + ], + operation: [ + 'qrCode', + ], + }, + }, + options: [ + { + displayName: 'Size', + name: 'size', + type: 'options', + options: [ + { + name: 'Small', + value: 'Small', + }, + { + name: 'Medium', + value: 'Medium', + }, + { + name: 'Large', + value: 'Large', + }, + ], + default: 'Small', + description: 'The QR Code size', + }, + { + displayName: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'PNG', + value: 'PNG', + }, + { + name: 'SVG', + value: 'SVG', + }, + ], + default: 'PNG', + description: 'The QR Code format', + }, + ], + }, + // website: screenshot + { + displayName: 'Webpage URL', + name: 'link', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'screenshot', + ], + resource: [ + 'website', + ], + }, + }, + default: '', + description: 'Link to webpage to convert', + }, + { + displayName: 'Download Screenshot?', + name: 'download', + type: 'boolean', + required: true, + displayOptions: { + show: { + operation: [ + 'screenshot', + ], + resource: [ + 'website', + ], + }, + }, + default: false, + description: 'Whether to download the screenshot or return a link to it', + }, + { + displayName: 'Put Output In Field', + name: 'output', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'screenshot', + ], + resource: [ + 'website', + ], + download: [ + true, + ], + }, + }, + default: 'data', + description: 'The name of the output field to put the binary file data in', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'website', + ], + operation: [ + 'screenshot', + ], + }, + }, + options: [ + { + displayName: 'Screen Size', + name: 'screen', + type: 'options', + options: [ + { + name: 'Phone', + value: 'phone', + }, + { + name: 'Phone Landscape', + value: 'phone-landscape', + }, + { + name: 'Retina', + value: 'retina', + }, + { + name: 'Tablet', + value: 'tablet', + }, + { + name: 'Tablet Landscape', + value: 'tablet-landscape', + }, + ], + default: '', + description: 'The screen size', + }, + { + displayName: 'Force Refresh', + name: 'force', + type: 'boolean', + default: false, + description: `Normally the API will reuse a previously taken screenshot of the URL to give a faster response. + This option allows you to retake the screenshot at that exact time, for those times when it's necessary`, + }, + { + displayName: 'Full Page', + name: 'fullpage', + type: 'boolean', + default: false, + description: 'The API takes a screenshot of the viewable area for the desired screen size. If you need a screenshot of the whole length of the page, use this option', + }, + ], + }, + // information: exchangeRate + { + displayName: 'Value', + name: 'value', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'exchangeRate', + ], + resource: [ + 'information', + ], + }, + }, + default: '', + description: 'Value to convert', + }, + { + displayName: 'From Currency', + name: 'fromCurrency', + type: 'string', + required: true, + placeholder: 'USD', + displayOptions: { + show: { + operation: [ + 'exchangeRate', + ], + resource: [ + 'information', + ], + }, + }, + default: '', + description: 'From Currency', + }, + { + displayName: 'To Currency', + name: 'toCurrency', + type: 'string', + placeholder: 'EUR', + required: true, + displayOptions: { + show: { + operation: [ + 'exchangeRate', + ], + resource: [ + 'information', + ], + }, + }, + default: '', + description: 'To Currency', + }, + // information: imageMetadata + { + displayName: 'Link To Image', + name: 'link', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'imageMetadata', + ], + resource: [ + 'information', + ], + }, + }, + default: '', + description: 'Image to get metadata from', + }, + // website: seo + { + displayName: 'Webpage URL', + name: 'link', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'seo', + ], + resource: [ + 'website', + ], + }, + }, + default: '', + description: 'Webpage to get SEO information for', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'website', + ], + operation: [ + 'seo', + ], + }, + }, + options: [ + { + displayName: 'Include Headers?', + name: 'headers', + type: 'boolean', + default: false, + description: '', + }, + ], + }, + // utility: validateEmail + { + displayName: 'Email Address', + name: 'emailAddress', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'validateEmail', + ], + resource: [ + 'utility', + ], + }, + }, + default: '', + description: 'Email Address', + }, + // utility: expandURL + { + displayName: 'URL', + name: 'link', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'expandURL', + ], + resource: [ + 'utility', + ], + }, + }, + default: '', + description: 'URL to unshorten', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + let download; + for (let i = 0; i < length; i++) { + try { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'website') { + if (operation === 'pdf') { + const link = this.getNodeParameter('link', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + download = this.getNodeParameter('download', i) as boolean; + qs.url = link; + + if (options.page) { + qs.page = options.page as string; + } + + if (options.force) { + qs.force = 'yes'; + } else { + qs.force = 'no'; + } + + const response = await oneSimpleApiRequest.call(this, 'GET', '/pdf', {}, qs); + + if (download) { + const output = this.getNodeParameter('output', i) as string; + const buffer = await oneSimpleApiRequest.call(this, 'GET', '', {}, {}, response.url, { json: false, encoding: null }) as Buffer; + responseData = { + json: response, + binary: { + [output]: await this.helpers.prepareBinaryData(buffer), + }, + }; + } else { + responseData = response; + } + } + + if (operation === 'screenshot') { + const link = this.getNodeParameter('link', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + download = this.getNodeParameter('download', i) as boolean; + + qs.url = link; + + if (options.screen) { + qs.screen = options.screen as string; + } + + if (options.fullpage) { + qs.fullpage = 'yes'; + } else { + qs.fullpage = 'no'; + } + + if (options.force) { + qs.force = 'yes'; + } else { + qs.force = 'no'; + } + + const response = await oneSimpleApiRequest.call(this, 'GET', '/screenshot', {}, qs); + + if (download) { + const output = this.getNodeParameter('output', i) as string; + const buffer = await oneSimpleApiRequest.call(this, 'GET', '', {}, {}, response.url, { json: false, encoding: null }) as Buffer; + responseData = { + json: response, + binary: { + [output]: await this.helpers.prepareBinaryData(buffer), + }, + }; + } else { + responseData = response; + } + } + + if (operation === 'seo') { + const link = this.getNodeParameter('link', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + qs.url = link; + + if (options.headers) { + qs.headers = 'yes'; + } + + responseData = await oneSimpleApiRequest.call(this, 'GET', '/page_info', {}, qs); + } + } + + if (resource === 'information') { + if (operation === 'exchangeRate') { + const value = this.getNodeParameter('value', i) as string; + const fromCurrency = this.getNodeParameter('fromCurrency', i) as string; + const toCurrency = this.getNodeParameter('toCurrency', i) as string; + qs.from_currency = fromCurrency; + qs.to_currency = toCurrency; + qs.from_value = value; + responseData = await oneSimpleApiRequest.call(this, 'GET', '/exchange_rate', {}, qs); + } + + if (operation === 'imageMetadata') { + const link = this.getNodeParameter('link', i) as string; + qs.url = link; + qs.raw = true; + responseData = await oneSimpleApiRequest.call(this, 'GET', '/image_info', {}, qs); + } + } + + if (resource === 'utility') { + // validateEmail + if (operation === 'validateEmail') { + const emailAddress = this.getNodeParameter('emailAddress', i) as string; + qs.email = emailAddress; + responseData = await oneSimpleApiRequest.call(this, 'GET', '/email', {}, qs); + } + // expandURL + if (operation === 'expandURL') { + const url = this.getNodeParameter('link', i) as string; + qs.url = url; + responseData = await oneSimpleApiRequest.call(this, 'GET', '/unshorten', {}, qs); + } + + if (operation === 'qrCode') { + const message = this.getNodeParameter('message', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + download = this.getNodeParameter('download', i) as boolean; + + qs.message = message; + + if (options.size) { + qs.size = options.size as string; + } + + if (options.format) { + qs.format = options.format as string; + } + + const response = await oneSimpleApiRequest.call(this, 'GET', '/qr_code', {}, qs); + + if (download) { + const output = this.getNodeParameter('output', i) as string; + const buffer = await oneSimpleApiRequest.call(this, 'GET', '', {}, {}, response.url, { json: false, encoding: null }) as Buffer; + responseData = { + json: response, + binary: { + [output]: await this.helpers.prepareBinaryData(buffer), + }, + }; + } else { + responseData = response; + } + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + + if (download) { + return this.prepareOutputData(returnData as unknown as INodeExecutionData[]); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/OneSimpleApi/onesimpleapi.svg b/packages/nodes-base/nodes/OneSimpleApi/onesimpleapi.svg new file mode 100644 index 0000000000..9a918f0a9c --- /dev/null +++ b/packages/nodes-base/nodes/OneSimpleApi/onesimpleapi.svg @@ -0,0 +1,25 @@ + diff --git a/packages/nodes-base/nodes/RespondToWebhook.node.json b/packages/nodes-base/nodes/RespondToWebhook.node.json new file mode 100644 index 0000000000..99bf635add --- /dev/null +++ b/packages/nodes-base/nodes/RespondToWebhook.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.respondToWebhook", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Core Nodes", + "Utility" + ], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.respondToWebhook/" + } + ] + }, + "subcategories": { + "Core Nodes":["Flow"] + } +} diff --git a/packages/nodes-base/nodes/Stripe/StripeTrigger.node.ts b/packages/nodes-base/nodes/Stripe/StripeTrigger.node.ts index 58829d9aea..12ac348165 100644 --- a/packages/nodes-base/nodes/Stripe/StripeTrigger.node.ts +++ b/packages/nodes-base/nodes/Stripe/StripeTrigger.node.ts @@ -820,7 +820,7 @@ export class StripeTrigger implements INodeType { try { await stripeApiRequest.call(this, 'GET', endpoint, {}); } catch (error) { - if (error.message.includes('resource_missing')) { + if (error.httpCode === '404' || error.message.includes('resource_missing')) { // Webhook does not exist delete webhookData.webhookId; delete webhookData.webhookEvents; diff --git a/packages/nodes-base/nodes/Toggl/GenericFunctions.ts b/packages/nodes-base/nodes/Toggl/GenericFunctions.ts index c694cf6008..b8386abb94 100644 --- a/packages/nodes-base/nodes/Toggl/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Toggl/GenericFunctions.ts @@ -25,7 +25,7 @@ export async function togglApiRequest(this: ITriggerFunctions | IPollFunctions | headers: headerWithAuthentication, method, qs: query, - uri: uri || `https://www.toggl.com/api/v8${resource}`, + uri: uri || `https://api.track.toggl.com/api/v8${resource}`, body, json: true, }; diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index 22718413d1..83c7850e74 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -1,8 +1,14 @@ +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, } from 'n8n-core'; import { + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, ILoadOptionsFunctions, INodeExecutionData, @@ -10,6 +16,7 @@ import { INodeType, INodeTypeDescription, NodeApiError, + NodeCredentialTestResult, NodeOperationError, } from 'n8n-workflow'; @@ -70,6 +77,7 @@ export class Zendesk implements INodeType { ], }, }, + testedBy: 'zendeskSoftwareApiTest', }, { name: 'zendeskOAuth2Api', @@ -146,6 +154,42 @@ export class Zendesk implements INodeType { }; methods = { + credentialTest: { + async zendeskSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { + const credentials = credential.data; + const subdomain = credentials!.subdomain; + const email = credentials!.email; + const apiToken = credentials!.apiToken; + + const base64Key = Buffer.from(`${email}/token:${apiToken}`).toString('base64'); + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${base64Key}`, + }, + method: 'GET', + uri: `https://${subdomain}.zendesk.com/api/v2/ticket_fields.json`, + qs: { + recent: 0, + }, + json: true, + timeout: 5000, + }; + + try { + await this.helpers.request!(options); + } catch (error) { + return { + status: 'Error', + message: `Connection details not valid: ${error.message}`, + }; + } + return { + status: 'OK', + message: 'Authentication successful!', + }; + }, + }, loadOptions: { // Get all the custom fields to display them to user so that he can // select them easily diff --git a/packages/nodes-base/nodes/Zulip/UserDescription.ts b/packages/nodes-base/nodes/Zulip/UserDescription.ts index 1890f71c9d..164432e5b0 100644 --- a/packages/nodes-base/nodes/Zulip/UserDescription.ts +++ b/packages/nodes-base/nodes/Zulip/UserDescription.ts @@ -226,14 +226,14 @@ export const userFields = [ name: 'isAdmin', type: 'boolean', default: false, - description: 'Whether the target user is an administrator.', + description: 'Whether the target user is an administrator', }, { displayName: 'Is Guest', name: 'isGuest', type: 'boolean', default: false, - description: 'Whether the target user is a guest.', + description: 'Whether the target user is a guest', }, { displayName: 'Profile Data', @@ -268,6 +268,35 @@ export const userFields = [ }, ], }, + { + displayName: 'Role', + name: 'role', + type: 'options', + options: [ + { + name: 'Organization Owner', + value: 100, + }, + { + name: 'Organization Administrator', + value: 200, + }, + { + name: 'Organization Moderator', + value: 300, + }, + { + name: 'Member', + value: 400, + }, + { + name: 'Guest', + value: 600, + }, + ], + default: '', + description: 'Role for the user', + }, ], }, diff --git a/packages/nodes-base/nodes/Zulip/UserInterface.ts b/packages/nodes-base/nodes/Zulip/UserInterface.ts index fd7dffcea9..12ff6d63b5 100644 --- a/packages/nodes-base/nodes/Zulip/UserInterface.ts +++ b/packages/nodes-base/nodes/Zulip/UserInterface.ts @@ -8,4 +8,5 @@ export interface IUser { email?: string; password?: string; short_name?: string; + role?: number; } diff --git a/packages/nodes-base/nodes/Zulip/Zulip.node.ts b/packages/nodes-base/nodes/Zulip/Zulip.node.ts index 9281e90dc9..1e9e9c9bd0 100644 --- a/packages/nodes-base/nodes/Zulip/Zulip.node.ts +++ b/packages/nodes-base/nodes/Zulip/Zulip.node.ts @@ -431,6 +431,9 @@ export class Zulip implements INodeType { if (additionalFields.isGuest) { body.is_guest = additionalFields.isGuest as boolean; } + if (additionalFields.role) { + body.role = additionalFields.role as number; + } if (additionalFields.profileData) { //@ts-ignore body.profile_data = additionalFields.profileData.properties as [{}]; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 3034a6ab2c..b17229cd90 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.145.0", + "version": "0.146.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -201,6 +201,7 @@ "dist/credentials/NotionOAuth2Api.credentials.js", "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", + "dist/credentials/OneSimpleApi.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/OrbitApi.credentials.js", "dist/credentials/OuraApi.credentials.js", @@ -520,6 +521,7 @@ "dist/nodes/Notion/NotionTrigger.node.js", "dist/nodes/N8nTrainingCustomerDatastore.node.js", "dist/nodes/N8nTrainingCustomerMessenger.node.js", + "dist/nodes/OneSimpleApi/OneSimpleApi.node.js", "dist/nodes/OpenThesaurus/OpenThesaurus.node.js", "dist/nodes/OpenWeatherMap.node.js", "dist/nodes/Orbit/Orbit.node.js", @@ -671,7 +673,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.75.0", + "n8n-workflow": "~0.76.0", "nodelinter": "^0.1.9", "ts-jest": "^26.3.0", "tslint": "^6.1.2", @@ -711,7 +713,7 @@ "mqtt": "4.2.6", "mssql": "^6.2.0", "mysql2": "~2.3.0", - "n8n-core": "~0.92.0", + "n8n-core": "~0.93.0", "node-ssh": "^12.0.0", "nodemailer": "^6.5.0", "pdf-parse": "^1.1.1", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 3027a02013..2e35eff0d3 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.75.0", + "version": "0.76.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index 6a92d2e26d..b7fd4d0138 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -99,6 +99,22 @@ export class Expression { ); const data = dataProxy.getDataProxy(); + // Support only a subset of process properties + // @ts-ignore + data.process = { + arch: process.arch, + env: process.env, + platform: process.platform, + pid: process.pid, + ppid: process.ppid, + release: process.release, + version: process.pid, + versions: process.versions, + }; + + // @ts-ignore + data.document = {}; + // Execute the expression try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call