diff --git a/.github/workflows/docker-images-rpi.yml b/.github/workflows/docker-images-rpi.yml index 155fd61158..1eeb66ecdb 100644 --- a/.github/workflows/docker-images-rpi.yml +++ b/.github/workflows/docker-images-rpi.yml @@ -4,6 +4,12 @@ on: push: tags: - n8n@* + workflow_dispatch: + inputs: + version: + description: 'n8n version to build docker image for.' + required: true + default: '0.112.0' jobs: armv7_job: @@ -28,7 +34,7 @@ jobs: run: | docker buildx build \ --platform linux/arm/v7 \ - --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} \ - -t ${{ secrets.DOCKER_USERNAME }}/n8n:${{steps.vars.outputs.tag}}-rpi \ + --build-arg N8N_VERSION=${{github.event.inputs.version || steps.vars.outputs.tag}} \ + -t ${{ secrets.DOCKER_USERNAME }}/n8n:${{github.event.inputs.version || steps.vars.outputs.tag}}-rpi \ -t ${{ secrets.DOCKER_USERNAME }}/n8n:latest-rpi \ --output type=image,push=true docker/images/n8n-rpi diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 6b5da8106b..1a9b81d09f 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -32,3 +32,7 @@ jobs: run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-debian docker/images/n8n-debian - name: Push Docker image of version (Debian) run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-debian + - name: Tag Docker image with latest (Debian) + run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}}-debian n8nio/n8n:latest-debian + - name: Push docker images of latest (Debian) + run: docker push n8nio/n8n:latest-debian diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 348ae253e0..4beac42b36 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [12.x, 14.x] steps: - uses: actions/checkout@v1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60516633a6..830a3b6ae5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,7 +139,7 @@ automatically build your code, restart the backend and refresh the frontend ``` npm run dev ``` -1. hack, hack, hack +1. Hack, hack, hack 1. Check if everything still runs in production mode ``` npm run build @@ -168,61 +168,28 @@ tests of all packages. ## Create Custom Nodes -It is very straightforward to create your own nodes for n8n. More information about that can -be found in the documentation of "n8n-node-dev" which is a small CLI which -helps with n8n-node-development. +Learn about [using the node dev CLI](https://docs.n8n.io/nodes/creating-nodes/node-dev-cli.html) to create custom nodes for n8n. -[To n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) +More information can +be found in the documentation of [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev), which is a small CLI which +helps with n8n-node-development. ## Create a new node to contribute to n8n -If you want to create a node which should be added to n8n follow these steps: +Follow this tutorial on [creating your first node](https://docs.n8n.io/nodes/creating-nodes/create-node.html) for n8n. - 1. Read the information in the [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) package as it contains a lot of generic information about node development. - - 1. Create the n8n development setup like described above and start n8n in develoment mode `npm run dev` - - 1. Create a new folder for the new node. For a service named "Example" the folder would be called: `/packages/nodes-base/nodes/Example` - - 1. If there is already a similar node, copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. - - 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to copy existing similar ones. - - 1. Add the path to the new node (and optionally credentials) to package.json of `nodes-base`. It already contains a property `n8n` with its own keys `credentials` and `nodes`. - - 1. Add icon for the node (60x60 PNG) - - 1. Start n8n. The new node will then be available via the editor UI and can be tested. - - -When developing n8n must get restarted and the browser reloaded every time parameters of a node change (like new ones added, removed or changed). Only then will the new data be loaded and the node displayed correctly. - -If only the code of the node changes (the execute method) than it is not needed as each workflow automatically starts a new process and so will always load the latest code. ## Checklist before submitting a new node -If you'd like to submit a new node, please go through the following checklist. This will help us be quicker to review and merge your PR. - -- [ ] Make failing requests to the API to ensure that the errors get displayed correctly (like malformed requests or requests with invalid credentials) -- [ ] Ensure that the default values do not change and that the parameters do not get renamed, as it would break the existing workflows of the users -- [ ] Ensure that all the top-level parameters use camelCase -- [ ] Ensure that all the options are ordered alphabetically, unless a different order is needed for a specific reason -- [ ] Ensure that the parameters have the correct type -- [ ] Make sure that the file-name and the Class name are identical (case sensitive). The name under "description" in the node-code should also be identical (except that it starts with a lower-case letter and that it will never have a space) -- [ ] Names of Trigger-Nodes always have to end with "Trigger" -- [ ] Add credentials and nodes to the `package.json` file in alphanumerical order -- [ ] Use tabs in all the files except in the `package.json` file, where 4-spaces have to get used -- [ ] To make it as simple as possible for the users, check other similar nodes to ensure that they all behave similarly -- [ ] Try to add as few parameters as possible on the main level to ensure that the node doesn't appear overwhelming. It should only contain the required parameters. All the other ones should be hidden on lower levels as "Additional Parameters" or "Options" -- [ ] Create only one node per service which can do everything via "Resource" and "Options" and not a separate one for each possible operation. +There are several things to keep in mind when creating a node. To help you, we prepared a [checklist](https://docs.n8n.io/nodes/creating-nodes/node-review-checklist.html) that covers the requirements for creating nodes, from preparation to submission. This will help us be quicker to review and merge your PR. ## Extend Documentation -The repository for the n8n documentation on https://docs.n8n.io can be found [here](https://github.com/n8n-io/n8n-docs). +The repository for the n8n documentation on [docs.n8n.io](https://docs.n8n.io) can be found [here](https://github.com/n8n-io/n8n-docs). ## Contributor License Agreement diff --git a/docker/images/n8n-rpi/Dockerfile b/docker/images/n8n-rpi/Dockerfile index 7497d532e6..329b7e7dfe 100644 --- a/docker/images/n8n-rpi/Dockerfile +++ b/docker/images/n8n-rpi/Dockerfile @@ -15,6 +15,7 @@ ENV NODE_ENV production WORKDIR /data -USER node +USER root -CMD n8n +CMD chown -R node:node /home/node/.n8n \ +&& gosu node n8n diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 44eddb69a4..aeb4f7e275 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,36 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.113.0 + +### What changed? +In the Dropbox node, both credential types (Access Token & OAuth2) have a new parameter called "APP Access Type". + +### When is action necessary? + +If you are using a Dropbox APP with permission type, "App Folder". + +### How to upgrade: + +Open your Dropbox node's credentials and set the "APP Access Type" parameter to "App Folder". + +## 0.111.0 + +### What changed? +In the Dropbox node, now all operations are performed relative to the user's root directory. + +### When is action necessary? + +If you are using any resource/operation with OAuth2 authentication. + +If you are using the `folder:list` operation with the parameter `Folder Path` empty (root path) and have a Team Space in your Dropbox account. + +### How to upgrade: + +Open the Dropbox node, go to the OAuth2 credential you are using and reconnect it again. + +Also, if you are using the `folder:list` operation, make sure your logic is taking into account the team folders in the response. + ## 0.105.0 ### What changed? diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 502312e400..90fad69345 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -21,7 +21,7 @@ import { WorkflowCredentials, WorkflowHelpers, WorkflowRunner, -} from "../src"; +} from '../src'; export class Execute extends Command { @@ -130,7 +130,7 @@ export class Execute extends Command { // Check if the workflow contains the required "Start" node // "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue const requiredNodeTypes = ['n8n-nodes-base.start']; - let startNode: INode | undefined= undefined; + let startNode: INode | undefined = undefined; for (const node of workflowData!.nodes) { if (requiredNodeTypes.includes(node.type)) { startNode = node; diff --git a/packages/cli/commands/export/credentials.ts b/packages/cli/commands/export/credentials.ts index 6f9038e6cd..ec12033446 100644 --- a/packages/cli/commands/export/credentials.ts +++ b/packages/cli/commands/export/credentials.ts @@ -3,6 +3,11 @@ import { flags, } from '@oclif/command'; +import { + Credentials, + UserSettings, +} from 'n8n-core'; + import { IDataObject } from 'n8n-workflow'; @@ -10,6 +15,7 @@ import { import { Db, GenericHelpers, + ICredentialsDecryptedDb, } from '../../src'; import * as fs from 'fs'; @@ -21,8 +27,9 @@ export class ExportCredentialsCommand extends Command { static examples = [ `$ n8n export:credentials --all`, `$ n8n export:credentials --id=5 --output=file.json`, - `$ n8n export:credentials --all --output=backups/latest/`, + `$ n8n export:credentials --all --output=backups/latest.json`, `$ n8n export:credentials --backup --output=backups/latest/`, + `$ n8n export:credentials --all --decrypted --output=backups/decrypted.json`, ]; static flags = { @@ -46,6 +53,9 @@ export class ExportCredentialsCommand extends Command { separate: flags.boolean({ description: 'Exports one file per credential (useful for versioning). Must inform a directory via --output.', }), + decrypted: flags.boolean({ + description: 'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).', + }), }; async run() { @@ -108,6 +118,20 @@ export class ExportCredentialsCommand extends Command { const credentials = await Db.collections.Credentials!.find(findQuery); + if (flags.decrypted) { + const encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + + for (let i = 0; i < credentials.length; i++) { + const { name, type, nodesAccess, data } = credentials[i]; + const credential = new Credentials(name, type, nodesAccess, data); + const plainData = credential.getData(encryptionKey); + (credentials[i] as ICredentialsDecryptedDb).data = plainData; + } + } + if (credentials.length === 0) { throw new Error('No credentials found with specified filters.'); } @@ -116,7 +140,7 @@ export class ExportCredentialsCommand extends Command { let fileContents: string, i: number; for (i = 0; i < credentials.length; i++) { fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined); - const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + ".json"; + const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json'; fs.writeFileSync(filename, fileContents); } console.log('Successfully exported', i, 'credentials.'); diff --git a/packages/cli/commands/export/workflow.ts b/packages/cli/commands/export/workflow.ts index 33066fd380..0cf1d712ad 100644 --- a/packages/cli/commands/export/workflow.ts +++ b/packages/cli/commands/export/workflow.ts @@ -116,7 +116,7 @@ export class ExportWorkflowsCommand extends Command { let fileContents: string, i: number; for (i = 0; i < workflows.length; i++) { fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined); - const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + ".json"; + const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json'; fs.writeFileSync(filename, fileContents); } console.log('Successfully exported', i, 'workflows.'); diff --git a/packages/cli/commands/import/credentials.ts b/packages/cli/commands/import/credentials.ts index 9d109c5b03..ed0da4e94b 100644 --- a/packages/cli/commands/import/credentials.ts +++ b/packages/cli/commands/import/credentials.ts @@ -3,6 +3,11 @@ import { flags, } from '@oclif/command'; +import { + Credentials, + UserSettings, +} from 'n8n-core'; + import { Db, GenericHelpers, @@ -51,10 +56,22 @@ export class ImportCredentialsCommand extends Command { try { await Db.init(); let i; + + const encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to encrypt the credentials!'); + } + if (flags.separate) { const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json'); for (i = 0; i < files.length; i++) { const credential = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' })); + + if (typeof credential.data === 'object') { + // plain data / decrypted input. Should be encrypted first. + Credentials.prototype.setData.call(credential, credential.data, encryptionKey); + } + await Db.collections.Credentials!.save(credential); } } else { @@ -65,6 +82,10 @@ export class ImportCredentialsCommand extends Command { } for (i = 0; i < fileContents.length; i++) { + if (typeof fileContents[i].data === 'object') { + // plain data / decrypted input. Should be encrypted first. + Credentials.prototype.setData.call(fileContents[i], fileContents[i].data, encryptionKey); + } await Db.collections.Credentials!.save(fileContents[i]); } } diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 112e751df9..c01ad0ed4a 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -17,11 +17,12 @@ import { Db, ExternalHooks, GenericHelpers, + IExecutionsCurrentSummary, LoadNodesAndCredentials, NodeTypes, Server, TestWebhooks, -} from "../src"; +} from '../src'; import { IDataObject } from 'n8n-workflow'; @@ -97,12 +98,15 @@ export class Start extends Command { // Wait for active workflow executions to finish const activeExecutionsInstance = ActiveExecutions.getInstance(); - let executingWorkflows = activeExecutionsInstance.getActiveExecutions(); + let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[]; let count = 0; while (executingWorkflows.length !== 0) { if (count++ % 4 === 0) { console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`); + executingWorkflows.map(execution => { + console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`); + }); } await new Promise((resolve) => { setTimeout(resolve, 500); @@ -129,7 +133,7 @@ export class Start extends Command { await (async () => { try { // Start directly with the init of the database to improve startup time - const startDbInitPromise = Db.init().catch(error => { + const startDbInitPromise = Db.init().catch((error: Error) => { console.error(`There was an error initializing DB: ${error.message}`); processExistCode = 1; @@ -168,7 +172,7 @@ export class Start extends Command { const redisDB = config.get('queue.bull.redis.db'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); let lastTimer = 0, cumulativeTimeout = 0; - + const settings = { retryStrategy: (times: number): number | null => { const now = Date.now(); @@ -180,7 +184,7 @@ export class Start extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); + console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.'); process.exit(1); } } @@ -200,7 +204,7 @@ export class Start extends Command { if (redisDB) { settings.db = redisDB; } - + // This connection is going to be our heartbeat // IORedis automatically pings redis and tries to reconnect // We will be using the retryStrategy above @@ -215,13 +219,13 @@ export class Start extends Command { } }); } - + const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; if (dbType === 'sqlite') { const shouldRunVacuum = config.get('database.sqlite.executeVacuumOnStartup') as number; if (shouldRunVacuum) { - Db.collections.Execution!.query("VACUUM;"); + Db.collections.Execution!.query('VACUUM;'); } } @@ -280,7 +284,7 @@ export class Start extends Command { Start.openBrowser(); } this.log(`\nPress "o" to open in Browser.`); - process.stdin.on("data", (key : string) => { + process.stdin.on('data', (key: string) => { if (key === 'o') { Start.openBrowser(); inputText = ''; diff --git a/packages/cli/commands/update/workflow.ts b/packages/cli/commands/update/workflow.ts index 6f9e8de2ff..06be05759d 100644 --- a/packages/cli/commands/update/workflow.ts +++ b/packages/cli/commands/update/workflow.ts @@ -9,7 +9,7 @@ import { import { Db, GenericHelpers, -} from "../../src"; +} from '../../src'; export class UpdateWorkflowCommand extends Command { diff --git a/packages/cli/commands/webhook.ts b/packages/cli/commands/webhook.ts index b17db917bd..68c5876543 100644 --- a/packages/cli/commands/webhook.ts +++ b/packages/cli/commands/webhook.ts @@ -17,7 +17,7 @@ import { NodeTypes, TestWebhooks, WebhookServer, -} from "../src"; +} from '../src'; import { IDataObject } from 'n8n-workflow'; @@ -98,7 +98,7 @@ export class Webhook extends Command { // Wrap that the process does not close but we can still use async await (async () => { if (config.get('executions.mode') !== 'queue') { - /** + /** * It is technically possible to run without queues but * there are 2 known bugs when running in this mode: * - Executions list will be problematic as the main process @@ -154,7 +154,7 @@ export class Webhook extends Command { const redisDB = config.get('queue.bull.redis.db'); const redisConnectionTimeoutLimit = config.get('queue.bull.redis.timeoutThreshold'); let lastTimer = 0, cumulativeTimeout = 0; - + const settings = { retryStrategy: (times: number): number | null => { const now = Date.now(); @@ -166,7 +166,7 @@ export class Webhook extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); + console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.'); process.exit(1); } } @@ -186,7 +186,7 @@ export class Webhook extends Command { if (redisDB) { settings.db = redisDB; } - + // This connection is going to be our heartbeat // IORedis automatically pings redis and tries to reconnect // We will be using the retryStrategy above @@ -201,7 +201,7 @@ export class Webhook extends Command { } }); } - + await WebhookServer.start(); // Start to get active workflows and run their triggers diff --git a/packages/cli/commands/worker.ts b/packages/cli/commands/worker.ts index aeb3b2ba08..573014cf62 100644 --- a/packages/cli/commands/worker.ts +++ b/packages/cli/commands/worker.ts @@ -35,7 +35,7 @@ import { ResponseHelper, WorkflowCredentials, WorkflowExecuteAdditionalData, -} from "../src"; +} from '../src'; import * as config from '../config'; import * as Bull from 'bull'; @@ -132,7 +132,7 @@ export class Worker extends Command { const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes); const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); - additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksIntegrated(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string }); + additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string }); let workflowExecute: WorkflowExecute; let workflowRun: PCancelable; @@ -241,7 +241,7 @@ export class Worker extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); + console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.'); process.exit(1); } } diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 8d2c412f08..0b136b52e4 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -446,6 +446,20 @@ const config = convict({ }, endpoints: { + metrics: { + enable: { + format: 'Boolean', + default: false, + env: 'N8N_METRICS', + doc: 'Enable metrics endpoint', + }, + prefix: { + format: String, + default: 'n8n_', + env: 'N8N_METRICS_PREFIX', + doc: 'An optional prefix for metric names. Default: n8n_', + }, + }, rest: { format: String, default: 'rest', @@ -471,7 +485,7 @@ const config = convict({ doc: 'Disable production webhooks from main process. This helps ensures no http traffic load to main process when using webhook-specific processes.', }, skipWebhoooksDeregistrationOnShutdown: { - /** + /** * Longer explanation: n8n deregisters webhooks on shutdown / deactivation * and registers on startup / activation. If we skip * deactivation on shutdown, webhooks will remain active on 3rd party services. @@ -483,10 +497,10 @@ const config = convict({ * WARNING: Trigger nodes (like Cron) will cause duplication * of work, so be aware when using. */ - doc: 'Deregister webhooks on external services only when workflows are deactivated. Useful for blue/green deployments.', + doc: 'Deregister webhooks on external services only when workflows are deactivated.', format: Boolean, default: false, - env: 'N8N_SKIP_WEBHOOK_DEREGISTRATION_STARTUP_SHUTDOWN', + env: 'N8N_SKIP_WEBHOOK_DEREGISTRATION_SHUTDOWN', }, }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 4f8d80ab39..410718dd12 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,139 +1,140 @@ { - "name": "n8n", - "version": "0.107.0", - "description": "n8n Workflow Automation Tool", - "license": "SEE LICENSE IN LICENSE.md", - "homepage": "https://n8n.io", - "author": { - "name": "Jan Oberhauser", - "email": "jan@n8n.io" + "name": "n8n", + "version": "0.114.0", + "description": "n8n Workflow Automation Tool", + "license": "SEE LICENSE IN LICENSE.md", + "homepage": "https://n8n.io", + "author": { + "name": "Jan Oberhauser", + "email": "jan@n8n.io" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/n8n-io/n8n.git" + }, + "main": "dist/index", + "types": "dist/src/index.d.ts", + "oclif": { + "commands": "./dist/commands", + "bin": "n8n" + }, + "scripts": { + "build": "tsc", + "dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"", + "postpack": "rm -f oclif.manifest.json", + "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", + "start": "run-script-os", + "start:default": "cd bin && ./n8n", + "start:windows": "cd bin && n8n", + "test": "jest", + "tslint": "tslint -p tsconfig.json -c tslint.json", + "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", + "watch": "tsc --watch", + "typeorm": "ts-node ./node_modules/typeorm/cli.js" + }, + "bin": { + "n8n": "./bin/n8n" + }, + "keywords": [ + "automate", + "automation", + "IaaS", + "iPaaS", + "n8n", + "workflow" + ], + "engines": { + "node": ">=12.0.0" + }, + "files": [ + "bin", + "templates", + "dist", + "oclif.manifest.json" + ], + "devDependencies": { + "@oclif/dev-cli": "^1.22.2", + "@types/basic-auth": "^1.1.2", + "@types/bcryptjs": "^2.4.1", + "@types/bull": "^3.3.10", + "@types/compression": "1.0.1", + "@types/connect-history-api-fallback": "^1.3.1", + "@types/convict": "^4.2.1", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.6", + "@types/jest": "^26.0.13", + "@types/localtunnel": "^1.9.0", + "@types/lodash.get": "^4.4.6", + "@types/node": "14.0.27", + "@types/open": "^6.1.0", + "@types/parseurl": "^1.3.1", + "@types/request-promise-native": "~1.0.15", + "concurrently": "^5.1.0", + "jest": "^26.4.2", + "nodemon": "^2.0.2", + "p-cancelable": "^2.0.0", + "run-script-os": "^1.0.7", + "ts-jest": "^26.3.0", + "ts-node": "^8.9.1", + "tslint": "^6.1.2", + "typescript": "~3.9.7" + }, + "dependencies": { + "@oclif/command": "^1.5.18", + "@oclif/errors": "^1.2.2", + "@types/json-diff": "^0.5.1", + "@types/jsonwebtoken": "^8.3.4", + "basic-auth": "^2.0.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.18.3", + "body-parser-xml": "^1.1.0", + "bull": "^3.19.0", + "client-oauth2": "^4.2.5", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "convict": "^6.0.1", + "csrf": "^3.1.0", + "dotenv": "^8.0.0", + "express": "^4.16.4", + "flatted": "^2.0.0", + "glob-promise": "^3.4.0", + "google-timezones-json": "^1.0.2", + "inquirer": "^7.0.1", + "json-diff": "^0.5.4", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "~1.12.1", + "localtunnel": "^2.0.0", + "lodash.get": "^4.4.2", + "mysql2": "~2.1.0", + "n8n-core": "~0.67.0", + "n8n-editor-ui": "~0.84.0", + "n8n-nodes-base": "~0.111.0", + "n8n-workflow": "~0.55.0", + "oauth-1.0a": "^2.2.6", + "open": "^7.0.0", + "pg": "^8.3.0", + "prom-client": "^13.1.0", + "request-promise-native": "^1.0.7", + "sqlite3": "^5.0.1", + "sse-channel": "^3.1.1", + "tslib": "1.11.2", + "typeorm": "^0.2.30" + }, + "jest": { + "transform": { + "^.+\\.tsx?$": "ts-jest" }, - "repository": { - "type": "git", - "url": "git+https://github.com/n8n-io/n8n.git" - }, - "main": "dist/index", - "types": "dist/src/index.d.ts", - "oclif": { - "commands": "./dist/commands", - "bin": "n8n" - }, - "scripts": { - "build": "tsc", - "dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"", - "postpack": "rm -f oclif.manifest.json", - "prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest", - "start": "run-script-os", - "start:default": "cd bin && ./n8n", - "start:windows": "cd bin && n8n", - "test": "jest", - "tslint": "tslint -p tsconfig.json -c tslint.json", - "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", - "watch": "tsc --watch", - "typeorm": "ts-node ./node_modules/typeorm/cli.js" - }, - "bin": { - "n8n": "./bin/n8n" - }, - "keywords": [ - "automate", - "automation", - "IaaS", - "iPaaS", - "n8n", - "workflow" + "testURL": "http://localhost/", + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "testPathIgnorePatterns": [ + "/dist/", + "/node_modules/" ], - "engines": { - "node": ">=12.0.0" - }, - "files": [ - "bin", - "templates", - "dist", - "oclif.manifest.json" - ], - "devDependencies": { - "@oclif/dev-cli": "^1.22.2", - "@types/basic-auth": "^1.1.2", - "@types/bcryptjs": "^2.4.1", - "@types/bull": "^3.3.10", - "@types/compression": "1.0.1", - "@types/connect-history-api-fallback": "^1.3.1", - "@types/convict": "^4.2.1", - "@types/dotenv": "^8.2.0", - "@types/express": "^4.17.6", - "@types/jest": "^26.0.13", - "@types/localtunnel": "^1.9.0", - "@types/lodash.get": "^4.4.6", - "@types/node": "14.0.27", - "@types/open": "^6.1.0", - "@types/parseurl": "^1.3.1", - "@types/request-promise-native": "~1.0.15", - "concurrently": "^5.1.0", - "jest": "^26.4.2", - "nodemon": "^2.0.2", - "p-cancelable": "^2.0.0", - "run-script-os": "^1.0.7", - "ts-jest": "^26.3.0", - "ts-node": "^8.9.1", - "tslint": "^6.1.2", - "typescript": "~3.9.7" - }, - "dependencies": { - "@oclif/command": "^1.5.18", - "@oclif/errors": "^1.2.2", - "@types/json-diff": "^0.5.1", - "@types/jsonwebtoken": "^8.3.4", - "basic-auth": "^2.0.1", - "bcryptjs": "^2.4.3", - "body-parser": "^1.18.3", - "body-parser-xml": "^1.1.0", - "bull": "^3.19.0", - "client-oauth2": "^4.2.5", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "convict": "^5.0.0", - "csrf": "^3.1.0", - "dotenv": "^8.0.0", - "express": "^4.16.4", - "flatted": "^2.0.0", - "glob-promise": "^3.4.0", - "google-timezones-json": "^1.0.2", - "inquirer": "^7.0.1", - "json-diff": "^0.5.4", - "jsonwebtoken": "^8.5.1", - "jwks-rsa": "~1.9.0", - "localtunnel": "^2.0.0", - "lodash.get": "^4.4.2", - "mysql2": "~2.1.0", - "n8n-core": "~0.62.0", - "n8n-editor-ui": "~0.77.0", - "n8n-nodes-base": "~0.104.0", - "n8n-workflow": "~0.51.0", - "oauth-1.0a": "^2.2.6", - "open": "^7.0.0", - "pg": "^8.3.0", - "request-promise-native": "^1.0.7", - "sqlite3": "^5.0.1", - "sse-channel": "^3.1.1", - "tslib": "1.11.2", - "typeorm": "^0.2.30" - }, - "jest": { - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testURL": "http://localhost/", - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "testPathIgnorePatterns": [ - "/dist/", - "/node_modules/" - ], - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json" - ] - } + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ] + } } diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 3b5ce249ef..3ab8c932ed 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -31,6 +31,7 @@ import { NodeHelpers, WebhookHttpMethod, Workflow, + WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -66,7 +67,7 @@ export class ActiveWorkflowRunner { for (const workflowData of workflowsData) { console.log(` - ${workflowData.name}`); try { - await this.add(workflowData.id.toString(), workflowData); + await this.add(workflowData.id.toString(), 'init', workflowData); console.log(` => Started`); } catch (error) { console.log(` => ERROR: Workflow could not be activated:`); @@ -273,7 +274,7 @@ export class ActiveWorkflowRunner { * @returns {Promise} * @memberof ActiveWorkflowRunner */ - async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): Promise { + async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData); let path = '' as string | undefined; @@ -319,10 +320,10 @@ export class ActiveWorkflowRunner { await Db.collections.Webhook?.insert(webhook); - const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, false); + const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false); if (webhookExists !== true) { // If webhook does not exist yet create it - await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, false); + await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, false); } } catch (error) { @@ -378,7 +379,7 @@ export class ActiveWorkflowRunner { const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData); for (const webhookData of webhooks) { - await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, false); + await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', false); } await WorkflowHelpers.saveStaticData(workflow); @@ -446,9 +447,9 @@ export class ActiveWorkflowRunner { * @returns {IGetExecutePollFunctions} * @memberof ActiveWorkflowRunner */ - getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecutePollFunctions { + getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecutePollFunctions { return ((workflow: Workflow, node: INode) => { - const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode); + const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation); returnFunctions.__emit = (data: INodeExecutionData[][]): void => { this.runWorkflow(workflowData, node, data, additionalData, mode); }; @@ -467,9 +468,9 @@ export class ActiveWorkflowRunner { * @returns {IGetExecuteTriggerFunctions} * @memberof ActiveWorkflowRunner */ - getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecuteTriggerFunctions{ + getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions{ return ((workflow: Workflow, node: INode) => { - const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode); + const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation); returnFunctions.emit = (data: INodeExecutionData[][]): void => { WorkflowHelpers.saveStaticData(workflow); this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err)); @@ -486,7 +487,7 @@ export class ActiveWorkflowRunner { * @returns {Promise} * @memberof ActiveWorkflowRunner */ - async add(workflowId: string, workflowData?: IWorkflowDb): Promise { + async add(workflowId: string, activation: WorkflowActivateMode, workflowData?: IWorkflowDb): Promise { if (this.activeWorkflows === null) { throw new Error(`The "activeWorkflows" instance did not get initialized yet.`); } @@ -511,15 +512,15 @@ export class ActiveWorkflowRunner { const mode = 'trigger'; const credentials = await WorkflowCredentials(workflowData.nodes); const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); - const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode); - const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode); + const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode, activation); + const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode, activation); // Add the workflows which have webhooks defined - await this.addWorkflowWebhooks(workflowInstance, additionalData, mode); + await this.addWorkflowWebhooks(workflowInstance, additionalData, mode, activation); if (workflowInstance.getTriggerNodes().length !== 0 || workflowInstance.getPollNodes().length !== 0) { - await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions, getPollFunctions); + await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions); } if (this.activationErrors[workflowId] !== undefined) { diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index cab67f7bce..74e9578fec 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -95,14 +95,15 @@ export async function getConfigValue(configKey: string): Promise { + const enableMetrics = config.get('endpoints.metrics.enable') as boolean; + let register: Registry; + + if (enableMetrics === true) { + const prefix = config.get('endpoints.metrics.prefix') as string; + register = new promClient.Registry(); + register.setDefaultLabels({ prefix }); + promClient.collectDefaultMetrics({ register }); + } + this.versions = await GenericHelpers.getVersions(); this.frontendSettings.versionCli = this.versions.cli; @@ -204,7 +214,7 @@ class App { const excludeEndpoints = config.get('security.excludeEndpoints') as string; - const ignoredEndpoints = ['healthz', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials]; + const ignoredEndpoints = ['healthz', 'metrics', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials]; ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':')); const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`); @@ -386,7 +396,7 @@ class App { this.app.use(history({ rewrites: [ { - from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), + from: new RegExp(`^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), to: (context) => { return context.parsedUrl!.pathname!.toString(); }, @@ -395,7 +405,8 @@ class App { })); //support application/x-www-form-urlencoded post data - this.app.use(bodyParser.urlencoded({ extended: false, + this.app.use(bodyParser.urlencoded({ + extended: false, verify: (req, res, buf) => { // @ts-ignore req.rawBody = buf; @@ -453,7 +464,16 @@ class App { ResponseHelper.sendSuccessResponse(res, responseData, true, 200); }); - + // ---------------------------------------- + // Metrics + // ---------------------------------------- + if (enableMetrics === true) { + this.app.get('/metrics', async (req: express.Request, res: express.Response) => { + const response = await register.metrics(); + res.setHeader('Content-Type', register.contentType); + ResponseHelper.sendSuccessResponse(res, response, true, 200); + }); + } // ---------------------------------------- // Workflow @@ -602,7 +622,7 @@ class App { try { await this.externalHooks.run('workflow.activate', [responseData]); - await this.activeWorkflowRunner.add(id); + await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate'); } catch (error) { // If workflow could not be activated set it again to inactive newWorkflowData.active = false; @@ -648,6 +668,7 @@ class App { const startNodes: string[] | undefined = req.body.startNodes; const destinationNode: string | undefined = req.body.destinationNode; const executionMode = 'manual'; + const activationMode = 'manual'; const sessionId = GenericHelpers.getSessionId(req); @@ -657,7 +678,7 @@ class App { const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const nodeTypes = NodeTypes(); const workflowInstance = new Workflow({ id: workflowData.id, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings }); - const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, sessionId, destinationNode); + const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, activationMode, sessionId, destinationNode); if (needsWebhook === true) { return { waitingForWebhook: true, @@ -725,7 +746,7 @@ class App { // Make a copy of the object. If we don't do this, then when // The method below is called the properties are removed for good // This happens because nodes are returned as reference. - const nodeInfo: INodeTypeDescription = {...nodeData.description}; + const nodeInfo: INodeTypeDescription = { ...nodeData.description }; if (req.query.includeProperties !== 'true') { // @ts-ignore delete nodeInfo.properties; @@ -1310,6 +1331,8 @@ class App { // Verify and store app code. Generate access tokens and store for respective credential. this.app.get(`/${this.restEndpoint}/oauth2-credential/callback`, async (req: express.Request, res: express.Response) => { + + // realmId it's currently just use for the quickbook OAuth2 flow const { code, state: stateEncoded } = req.query; if (code === undefined || stateEncoded === undefined) { @@ -1384,6 +1407,10 @@ class App { const oauthToken = await oAuthObj.code.getToken(`${oAuth2Parameters.redirectUri}?${queryParameters}`, options); + if (Object.keys(req.query).length > 2) { + _.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code')); + } + if (oauthToken === undefined) { const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); return ResponseHelper.sendErrorResponse(res, errorResponse); @@ -1430,14 +1457,14 @@ class App { limit = parseInt(req.query.limit as string, 10); } - let executingWorkflowIds; + const executingWorkflowIds: string[] = []; if (config.get('executions.mode') === 'queue') { const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); - executingWorkflowIds = currentJobs.map(job => job.data.executionId) as string[]; - } else { - executingWorkflowIds = this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]; + executingWorkflowIds.push(...currentJobs.map(job => job.data.executionId) as string[]); } + // We may have manual executions even with queue so we must account for these. + executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]); const countFilter = JSON.parse(JSON.stringify(filter)); countFilter.select = ['id']; @@ -1472,7 +1499,7 @@ class App { } const resultsPromise = resultsQuery.getMany(); - + const countPromise = Db.collections.Execution!.count(countFilter); const results: IExecutionFlattedDb[] = await resultsPromise; @@ -1510,7 +1537,7 @@ class App { } if (req.query.unflattedResponse === 'true') { - const fullExecutionData = ResponseHelper.unflattenExecutionData(result); + const fullExecutionData = ResponseHelper.unflattenExecutionData(result); return fullExecutionData as IExecutionResponse; } else { // Convert to response format in which the id is a string @@ -1557,7 +1584,7 @@ class App { delete data!.executionData!.resultData.error; const length = data!.executionData!.resultData.runData[lastNodeExecuted].length; if (length > 0 && data!.executionData!.resultData.runData[lastNodeExecuted][length - 1].error !== undefined) { - // Remove results only if it is an error. + // Remove results only if it is an error. // If we are retrying due to a crash, the information is simply success info from last node data!.executionData!.resultData.runData[lastNodeExecuted].pop(); // Stack will determine what to run next @@ -1638,7 +1665,16 @@ class App { if (config.get('executions.mode') === 'queue') { const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); - const currentlyRunningExecutionIds = currentJobs.map(job => job.data.executionId); + const currentlyRunningQueueIds = currentJobs.map(job => job.data.executionId); + + const currentlyRunningManualExecutions = this.activeExecutionsInstance.getActiveExecutions(); + const manualExecutionIds = currentlyRunningManualExecutions.map(execution => execution.id); + + const currentlyRunningExecutionIds = currentlyRunningQueueIds.concat(manualExecutionIds); + + if (currentlyRunningExecutionIds.length === 0) { + return []; + } const resultsQuery = await Db.collections.Execution! .createQueryBuilder("execution") @@ -1651,7 +1687,7 @@ class App { ]) .orderBy('execution.id', 'DESC') .andWhere(`execution.id IN (:...ids)`, {ids: currentlyRunningExecutionIds}); - + if (req.query.filter) { const filter = JSON.parse(req.query.filter as string); if (filter.workflowId !== undefined) { @@ -1663,7 +1699,7 @@ class App { return results.map(result => { return { - idActive: result.id, + id: result.id, workflowId: result.workflowId, mode: result.mode, retryOf: result.retryOf !== null ? result.retryOf : undefined, @@ -1674,27 +1710,27 @@ class App { const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions(); const returnData: IExecutionsSummary[] = []; - + let filter: any = {}; // tslint:disable-line:no-any if (req.query.filter) { filter = JSON.parse(req.query.filter as string); } - + for (const data of executingWorkflows) { if (filter.workflowId !== undefined && filter.workflowId !== data.workflowId) { continue; } returnData.push( { - idActive: data.id.toString(), - workflowId: data.workflowId.toString(), + id: data.id.toString(), + workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(), mode: data.mode, retryOf: data.retryOf, startedAt: new Date(data.startedAt), } ); } - + return returnData; } })); @@ -1702,6 +1738,20 @@ class App { // Forces the execution to stop this.app.post(`/${this.restEndpoint}/executions-current/:id/stop`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { if (config.get('executions.mode') === 'queue') { + // Manual executions should still be stoppable, so + // try notifying the `activeExecutions` to stop it. + const result = await this.activeExecutionsInstance.stopExecution(req.params.id); + if (result !== undefined) { + const returnData: IExecutionsStopData = { + mode: result.mode, + startedAt: new Date(result.startedAt), + stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, + finished: result.finished, + }; + + return returnData; + } + const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']); const job = currentJobs.find(job => job.data.executionId.toString() === req.params.id); @@ -1721,26 +1771,26 @@ class App { stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined, finished: fullExecutionData.finished, }; - + return returnData; - + } else { const executionId = req.params.id; - + // Stopt he execution and wait till it is done and we got the data const result = await this.activeExecutionsInstance.stopExecution(executionId); - + if (result === undefined) { throw new Error(`The execution id "${executionId}" could not be found.`); } - + const returnData: IExecutionsStopData = { mode: result.mode, startedAt: new Date(result.startedAt), stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, finished: result.finished, }; - + return returnData; } })); diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index e5caef9d93..b9dcf09fcb 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -17,6 +17,7 @@ import { IWorkflowExecuteAdditionalData, WebhookHttpMethod, Workflow, + WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -161,7 +162,7 @@ export class TestWebhooks { * @returns {(Promise)} * @memberof TestWebhooks */ - async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, sessionId?: string, destinationNode?: string): Promise { + async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, sessionId?: string, destinationNode?: string): Promise { const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode); if (webhooks.length === 0) { @@ -193,7 +194,7 @@ export class TestWebhooks { }; try { - await this.activeWebhooks!.add(workflow, webhookData, mode); + await this.activeWebhooks!.add(workflow, webhookData, mode, activation); } catch (error) { activatedKey.forEach(deleteKey => delete this.testWebhookData[deleteKey] ); await this.activeWebhooks!.removeWorkflow(workflow); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 2ef00c0473..92f6a93afc 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1,4 +1,5 @@ import { + ActiveExecutions, CredentialsHelper, Db, ExternalHooks, @@ -108,51 +109,15 @@ function pruneExecutionData(): void { // throttle just on success to allow for self healing on failure Db.collections.Execution!.delete({ stoppedAt: LessThanOrEqual(date.toISOString()) }) - .then(data => - setTimeout(() => { - throttling = false; - }, timeout * 1000) - ).catch(err => throttling = false); + .then(data => + setTimeout(() => { + throttling = false; + }, timeout * 1000) + ).catch(err => throttling = false); } } -/** - * Pushes the execution out to all connected clients - * - * @param {WorkflowExecuteMode} mode The mode in which the workflow got started in - * @param {IRun} fullRunData The RunData of the finished execution - * @param {string} executionIdActive The id of the finished execution - * @param {string} [executionIdDb] The database id of finished execution - */ -export function pushExecutionFinished(mode: WorkflowExecuteMode, fullRunData: IRun, executionIdActive: string, executionIdDb?: string, retryOf?: string) { - // Clone the object except the runData. That one is not supposed - // to be send. Because that data got send piece by piece after - // each node which finished executing - const pushRunData = { - ...fullRunData, - data: { - ...fullRunData.data, - resultData: { - ...fullRunData.data.resultData, - runData: {}, - }, - }, - }; - - // Push data to editor-ui once workflow finished - const sendData: IPushDataExecutionFinished = { - executionIdActive, - executionIdDb, - data: pushRunData, - retryOf, - }; - - const pushInstance = Push.getInstance(); - pushInstance.send('executionFinished', sendData); -} - - /** * Returns hook functions to push data to Editor-UI * @@ -192,25 +157,52 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { ], workflowExecuteBefore: [ async function (this: WorkflowHooks): Promise { - // Push data to editor-ui once workflow finished - if (this.mode === 'manual') { - const pushInstance = Push.getInstance(); - pushInstance.send('executionStarted', { - executionId: this.executionId, - mode: this.mode, - startedAt: new Date(), - retryOf: this.retryOf, - workflowId: this.workflowData.id as string, - workflowName: this.workflowData.name, - }); + // Push data to session which started the workflow + if (this.sessionId === undefined) { + return; } + const pushInstance = Push.getInstance(); + pushInstance.send('executionStarted', { + executionId: this.executionId, + mode: this.mode, + startedAt: new Date(), + retryOf: this.retryOf, + workflowId: this.workflowData.id as string, + workflowName: this.workflowData.name, + }, this.sessionId); }, ], workflowExecuteAfter: [ async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { - if (this.mode === 'manual') { - pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf); + // Push data to session which started the workflow + if (this.sessionId === undefined) { + return; } + + // Clone the object except the runData. That one is not supposed + // to be send. Because that data got send piece by piece after + // each node which finished executing + const pushRunData = { + ...fullRunData, + data: { + ...fullRunData.data, + resultData: { + ...fullRunData.data.resultData, + runData: {}, + }, + }, + }; + + // Push data to editor-ui once workflow finished + // TODO: Look at this again + const sendData: IPushDataExecutionFinished = { + executionId: this.executionId, + data: pushRunData, + retryOf: this.retryOf, + }; + + const pushInstance = Push.getInstance(); + pushInstance.send('executionFinished', sendData, this.sessionId); }, ], }; @@ -238,59 +230,70 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx return; } - const execution = await Db.collections.Execution!.findOne(this.executionId); + try { + const execution = await Db.collections.Execution!.findOne(this.executionId); - if (execution === undefined) { - // Something went badly wrong if this happens. - // This check is here mostly to make typescript happy. - return undefined; - } - const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution); + if (execution === undefined) { + // Something went badly wrong if this happens. + // This check is here mostly to make typescript happy. + return undefined; + } + const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution); - if (fullExecutionData.finished) { - // We already received ´workflowExecuteAfter´ webhook, so this is just an async call - // that was left behind. We skip saving because the other call should have saved everything - // so this one is safe to ignore - return; + if (fullExecutionData.finished) { + // We already received ´workflowExecuteAfter´ webhook, so this is just an async call + // that was left behind. We skip saving because the other call should have saved everything + // so this one is safe to ignore + return; + } + + + if (fullExecutionData.data === undefined) { + fullExecutionData.data = { + startData: { + }, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], + waitingExecution: {}, + }, + }; + } + + if (Array.isArray(fullExecutionData.data.resultData.runData[nodeName])) { + // Append data if array exists + fullExecutionData.data.resultData.runData[nodeName].push(data); + } else { + // Initialize array and save data + fullExecutionData.data.resultData.runData[nodeName] = [data]; + } + + fullExecutionData.data.executionData = executionData.executionData; + + // Set last executed node so that it may resume on failure + fullExecutionData.data.resultData.lastNodeExecuted = nodeName; + + const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData); + + await Db.collections.Execution!.update(this.executionId, flattenedExecutionData as IExecutionFlattedDb); + } catch (err) { + // TODO: Improve in the future! + // Errors here might happen because of database access + // For busy machines, we may get "Database is locked" errors. + + // We do this to prevent crashes and executions ending in `unknown` state. + console.log(`Failed saving execution progress to database for execution ID ${this.executionId}`, err); } - - if (fullExecutionData.data === undefined) { - fullExecutionData.data = { - startData: { - }, - resultData: { - runData: {}, - }, - executionData: { - contextData: {}, - nodeExecutionStack: [], - waitingExecution: {}, - }, - }; - } - - if (Array.isArray(fullExecutionData.data.resultData.runData[nodeName])) { - // Append data if array exists - fullExecutionData.data.resultData.runData[nodeName].push(data); - } else { - // Initialize array and save data - fullExecutionData.data.resultData.runData[nodeName] = [data]; - } - - fullExecutionData.data.executionData = executionData.executionData; - - // Set last executed node so that it may resume on failure - fullExecutionData.data.resultData.lastNodeExecuted = nodeName; - - const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData); - - await Db.collections.Execution!.update(this.executionId, flattenedExecutionData as IExecutionFlattedDb); }, ], }; } + /** * Returns hook functions to save workflow execution and call error workflow * @@ -330,7 +333,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { if (isManualMode && saveManualExecutions === false) { // Data is always saved, so we remove from database - Db.collections.Execution!.delete(this.executionId); + await Db.collections.Execution!.delete(this.executionId); return; } @@ -350,7 +353,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); } // Data is always saved, so we remove from database - Db.collections.Execution!.delete(this.executionId); + await Db.collections.Execution!.delete(this.executionId); return; } @@ -397,57 +400,79 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { /** - * Executes the workflow with the given ID + * Returns hook functions to save workflow execution and call error workflow + * for running with queues. Manual executions should never run on queues as + * they are always executed in the main process. * - * @export - * @param {string} workflowId The id of the workflow to execute - * @param {IWorkflowExecuteAdditionalData} additionalData - * @param {INodeExecutionData[]} [inputData] - * @returns {(Promise>)} + * @returns {IWorkflowExecuteHooks} */ -export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]): Promise> { +function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { + return { + nodeExecuteBefore: [], + nodeExecuteAfter: [], + workflowExecuteBefore: [], + workflowExecuteAfter: [ + async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { + try { + if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) === true && newStaticData) { + // Workflow is saved so update in database + try { + await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); + } catch (e) { + // TODO: Add proper logging! + console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`); + } + } + + // Check config to know if execution should be saved or not + let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; + if (this.workflowData.settings !== undefined) { + saveDataErrorExecution = (this.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; + } + + const workflowDidSucceed = !fullRunData.data.resultData.error; + if (workflowDidSucceed === false && saveDataErrorExecution === 'none') { + executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + } + + const fullExecutionData: IExecutionDb = { + data: fullRunData.data, + mode: fullRunData.mode, + finished: fullRunData.finished ? fullRunData.finished : false, + startedAt: fullRunData.startedAt, + stoppedAt: fullRunData.stoppedAt, + workflowData: this.workflowData, + }; + + if (this.retryOf !== undefined) { + fullExecutionData.retryOf = this.retryOf.toString(); + } + + if (this.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) === true) { + fullExecutionData.workflowId = this.workflowData.id.toString(); + } + + const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); + + // Save the Execution in DB + await Db.collections.Execution!.update(this.executionId, executionData as IExecutionFlattedDb); + + if (fullRunData.finished === true && this.retryOf !== undefined) { + // If the retry was successful save the reference it on the original execution + // await Db.collections.Execution!.save(executionData as IExecutionFlattedDb); + await Db.collections.Execution!.update(this.retryOf, { retrySuccessId: this.executionId }); + } + } catch (error) { + executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + } + }, + ], + }; +} + +export async function getRunData(workflowData: IWorkflowBase, inputData?: INodeExecutionData[]): Promise { const mode = 'integrated'; - if (workflowInfo.id === undefined && workflowInfo.code === undefined) { - throw new Error(`No information about the workflow to execute found. Please provide either the "id" or "code"!`); - } - - if (Db.collections!.Workflow === null) { - // The first time executeWorkflow gets called the Database has - // to get initialized first - await Db.init(); - } - - let workflowData: IWorkflowBase | undefined; - if (workflowInfo.id !== undefined) { - workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id); - if (workflowData === undefined) { - throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); - } - } else { - workflowData = workflowInfo.code; - } - - const externalHooks = ExternalHooks(); - await externalHooks.init(); - - const nodeTypes = NodeTypes(); - - const workflowName = workflowData ? workflowData.name : undefined; - const workflow = new Workflow({ id: workflowInfo.id, name: workflowName, nodes: workflowData!.nodes, connections: workflowData!.connections, active: workflowData!.active, nodeTypes, staticData: workflowData!.staticData }); - - // Does not get used so set it simply to empty string - const executionId = ''; - - // Get the needed credentials for the current workflow as they will differ to the ones of the - // calling workflow. - const credentials = await WorkflowCredentials(workflowData!.nodes); - - // Create new additionalData to have different workflow loaded and to call - // different webooks - const additionalDataIntegrated = await getBase(credentials); - additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode }); - // Find Start-Node const requiredNodeTypes = ['n8n-nodes-base.start']; let startNode: INode | undefined; @@ -494,17 +519,113 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi }, }; + // Get the needed credentials for the current workflow as they will differ to the ones of the + // calling workflow. + const credentials = await WorkflowCredentials(workflowData!.nodes); + + const runData: IWorkflowExecutionDataProcess = { + credentials, + executionMode: mode, + executionData: runExecutionData, + // @ts-ignore + workflowData, + }; + + return runData; +} + + +export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promise { + if (workflowInfo.id === undefined && workflowInfo.code === undefined) { + throw new Error(`No information about the workflow to execute found. Please provide either the "id" or "code"!`); + } + + if (Db.collections!.Workflow === null) { + // The first time executeWorkflow gets called the Database has + // to get initialized first + await Db.init(); + } + + let workflowData: IWorkflowBase | undefined; + if (workflowInfo.id !== undefined) { + workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id); + if (workflowData === undefined) { + throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); + } + } else { + workflowData = workflowInfo.code; + } + + return workflowData!; +} + + +/** + * Executes the workflow with the given ID + * + * @export + * @param {string} workflowId The id of the workflow to execute + * @param {IWorkflowExecuteAdditionalData} additionalData + * @param {INodeExecutionData[]} [inputData] + * @returns {(Promise>)} + */ +export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise | IRun> { + const externalHooks = ExternalHooks(); + await externalHooks.init(); + + const nodeTypes = NodeTypes(); + + const workflowData = loadedWorkflowData !== undefined ? loadedWorkflowData : await getWorkflowData(workflowInfo); + + const workflowName = workflowData ? workflowData.name : undefined; + const workflow = new Workflow({ id: workflowInfo.id, name: workflowName, nodes: workflowData!.nodes, connections: workflowData!.connections, active: workflowData!.active, nodeTypes, staticData: workflowData!.staticData }); + + const runData = loadedRunData !== undefined ? loadedRunData : await getRunData(workflowData, inputData); + + let executionId; + + if (parentExecutionId !== undefined) { + executionId = parentExecutionId; + } else { + executionId = parentExecutionId !== undefined ? parentExecutionId : await ActiveExecutions.getInstance().add(runData); + } + + const runExecutionData = runData.executionData as IRunExecutionData; + + // Get the needed credentials for the current workflow as they will differ to the ones of the + // calling workflow. + const credentials = await WorkflowCredentials(workflowData!.nodes); + + + // Create new additionalData to have different workflow loaded and to call + // different webooks + const additionalDataIntegrated = await getBase(credentials); + additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(runData.executionMode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode }); + // Make sure we pass on the original executeWorkflow function we received + // This one already contains changes to talk to parent process + // and get executionID from `activeExecutions` running on main process + additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow; + + // Execute the workflow - const workflowExecute = new WorkflowExecute(additionalDataIntegrated, mode, runExecutionData); + const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData); const data = await workflowExecute.processRunExecutionData(workflow); await externalHooks.run('workflow.postExecute', [data, workflowData]); if (data.finished === true) { // Workflow did finish successfully - const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); - return returnData!.data!.main; + + if (parentExecutionId !== undefined) { + return data; + } else { + await ActiveExecutions.getInstance().remove(executionId, data); + + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); + return returnData!.data!.main; + } } else { + await ActiveExecutions.getInstance().remove(executionId, data); // Workflow did fail const error = new Error(data.data.resultData.error!.message); error.stack = data.data.resultData.error!.stack; @@ -564,6 +685,22 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); } +/** + * Returns WorkflowHooks instance for running integrated workflows + * (Workflows which get started inside of another workflow) + */ +export function getWorkflowHooksWorkerExecuter(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { + optionalParameters = optionalParameters || {}; + const hookFunctions = hookFunctionsSaveWorker(); + const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); + for (const key of Object.keys(preExecuteFunctions)) { + if (hookFunctions[key] === undefined) { + hookFunctions[key] = []; + } + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } + return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); +} /** * Returns WorkflowHooks instance for main process if workflow runs via worker @@ -616,6 +753,6 @@ export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, execut } } - return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string}); + return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string }); } diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 5338a4a46d..f884ac5dcc 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -101,9 +101,6 @@ export class WorkflowRunner { // Remove from active execution with empty data. That will // set the execution to failed. this.activeExecutions.remove(executionId, fullRunData); - - // Also send to Editor UI - WorkflowExecuteAdditionalData.pushExecutionFinished(executionMode, fullRunData, executionId); } /** @@ -175,7 +172,7 @@ export class WorkflowRunner { workflowExecution = workflowExecute.processRunExecutionData(workflow); } else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) { // Execute all nodes - + // Can execute without webhook so go on const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); @@ -298,7 +295,7 @@ export class WorkflowRunner { }, queueRecoveryInterval * 1000); }); - + const clearWatchdogInterval = () => { if (watchDogInterval) { clearInterval(watchDogInterval); @@ -332,7 +329,7 @@ export class WorkflowRunner { await jobData; } - + const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb; const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; @@ -349,7 +346,27 @@ export class WorkflowRunner { // Normally also static data should be supplied here but as it only used for sending // data to editor-UI is not needed. hooks.executeHookFunctions('workflowExecuteAfter', [runData]); + try { + // Check if this execution data has to be removed from database + // based on workflow settings. + let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; + let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; + if (data.workflowData.settings !== undefined) { + saveDataErrorExecution = (data.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; + saveDataSuccessExecution = (data.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; + } + const workflowDidSucceed = !runData.data.resultData.error; + if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || + workflowDidSucceed === false && saveDataErrorExecution === 'none' + ) { + await Db.collections.Execution!.delete(executionId); + } + } catch (err) { + // We don't want errors here to crash n8n. Just log and proceed. + console.log('Error removing saved execution from database. More details: ', err); + } + resolve(runData); }); @@ -440,7 +457,7 @@ export class WorkflowRunner { // Listen to data from the subprocess - subprocess.on('message', (message: IProcessMessage) => { + subprocess.on('message', async (message: IProcessMessage) => { if (message.type === 'end') { clearTimeout(executionTimeout); this.activeExecutions.remove(executionId!, message.data.runData); @@ -457,6 +474,11 @@ export class WorkflowRunner { const timeoutError = { message: 'Workflow execution timed out!' } as IExecutionError; this.processError(timeoutError, startedAt, data.executionMode, executionId); + } else if (message.type === 'startExecution') { + const executionId = await this.activeExecutions.add(message.data.runData); + subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage); + } else if (message.type === 'finishExecution') { + await this.activeExecutions.remove(message.data.executionId, message.data.result); } }); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 71780e5ceb..70c140a7e3 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -7,6 +7,7 @@ import { IWorkflowExecutionDataProcessWithExecution, NodeTypes, WorkflowExecuteAdditionalData, + WorkflowHelpers, } from './'; import { @@ -17,12 +18,15 @@ import { import { IDataObject, IExecuteData, + IExecuteWorkflowInfo, IExecutionError, + INodeExecutionData, INodeType, INodeTypeData, IRun, IRunExecutionData, ITaskData, + IWorkflowExecuteAdditionalData, IWorkflowExecuteHooks, Workflow, WorkflowHooks, @@ -35,9 +39,20 @@ export class WorkflowRunnerProcess { startedAt = new Date(); workflow: Workflow | undefined; workflowExecute: WorkflowExecute | undefined; + executionIdCallback: (executionId: string) => void | undefined; + + static async stopProcess() { + setTimeout(() => { + // Attempt a graceful shutdown, giving executions 30 seconds to finish + process.exit(0); + }, 30000); + } async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise { + process.on('SIGTERM', WorkflowRunnerProcess.stopProcess); + process.on('SIGINT', WorkflowRunnerProcess.stopProcess); + this.data = inputData; let className: string; let tempNode: INodeType; @@ -92,16 +107,41 @@ export class WorkflowRunnerProcess { await Db.init(); } - this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings}); + this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings }); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); additionalData.hooks = this.getProcessForwardHooks(); + const executeWorkflowFunction = additionalData.executeWorkflow; + additionalData.executeWorkflow = async (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[] | undefined): Promise | IRun> => { + const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(workflowInfo); + const runData = await WorkflowExecuteAdditionalData.getRunData(workflowData, inputData); + await sendToParentProcess('startExecution', { runData }); + const executionId: string = await new Promise((resolve) => { + this.executionIdCallback = (executionId: string) => { + resolve(executionId); + }; + }); + let result: IRun; + try { + result = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData); + } catch (e) { + await sendToParentProcess('finishExecution', { executionId }); + // Throw same error we had + throw e; + } + + await sendToParentProcess('finishExecution', { executionId, result }); + + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result); + return returnData!.data!.main; + }; + if (this.data.executionData !== undefined) { this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode, this.data.executionData); return this.workflowExecute.processRunExecutionData(this.workflow); } else if (this.data.runData === undefined || this.data.startNodes === undefined || this.data.startNodes.length === 0 || this.data.destinationNode === undefined) { // Execute all nodes - + // Can execute without webhook so go on this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode); return this.workflowExecute.run(this.workflow, undefined, this.data.destinationNode); @@ -152,8 +192,8 @@ export class WorkflowRunnerProcess { }, ], nodeExecuteAfter: [ - async (nodeName: string, data: ITaskData, executionData: IRunExecutionData): Promise => { - this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data, executionData]); + async (nodeName: string, data: ITaskData): Promise => { + this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]); }, ], workflowExecuteBefore: [ @@ -257,6 +297,8 @@ process.on('message', async (message: IProcessMessage) => { // Stop process process.exit(); + } else if (message.type === 'executionId') { + workflowRunner.executionIdCallback(message.data.executionId); } } catch (error) { // Catch all uncaught errors and forward them to parent process diff --git a/packages/cli/src/databases/mysqldb/migrations/1615306975123-ChangeDataSize.ts b/packages/cli/src/databases/mysqldb/migrations/1615306975123-ChangeDataSize.ts new file mode 100644 index 0000000000..793b178ef3 --- /dev/null +++ b/packages/cli/src/databases/mysqldb/migrations/1615306975123-ChangeDataSize.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import * as config from '../../../../config'; + +export class ChangeDataSize1615306975123 implements MigrationInterface { + name = 'ChangeDataSize1615306975123'; + + async up(queryRunner: QueryRunner): Promise { + const tablePrefix = config.get('database.tablePrefix'); + + await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` MODIFY COLUMN `data` MEDIUMTEXT NOT NULL'); + } + + async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.get('database.tablePrefix'); + + await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` MODIFY COLUMN `data` TEXT NOT NULL'); + } +} diff --git a/packages/cli/src/databases/mysqldb/migrations/index.ts b/packages/cli/src/databases/mysqldb/migrations/index.ts index 4c736d57ed..08ca45edbf 100644 --- a/packages/cli/src/databases/mysqldb/migrations/index.ts +++ b/packages/cli/src/databases/mysqldb/migrations/index.ts @@ -3,6 +3,7 @@ import { WebhookModel1592447867632 } from './1592447867632-WebhookModel'; import { CreateIndexStoppedAt1594902918301 } from './1594902918301-CreateIndexStoppedAt'; import { AddWebhookId1611149998770 } from './1611149998770-AddWebhookId'; import { MakeStoppedAtNullable1607431743767 } from './1607431743767-MakeStoppedAtNullable'; +import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize'; export const mysqlMigrations = [ InitialMigration1588157391238, @@ -10,4 +11,5 @@ export const mysqlMigrations = [ CreateIndexStoppedAt1594902918301, AddWebhookId1611149998770, MakeStoppedAtNullable1607431743767, + ChangeDataSize1615306975123, ]; diff --git a/packages/core/package.json b/packages/core/package.json index 98dc4eb259..f0eee7085c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,74 +1,74 @@ { - "name": "n8n-core", - "version": "0.62.0", - "description": "Core functionality of n8n", - "license": "SEE LICENSE IN LICENSE.md", - "homepage": "https://n8n.io", - "author": { - "name": "Jan Oberhauser", - "email": "jan@n8n.io" + "name": "n8n-core", + "version": "0.67.0", + "description": "Core functionality of n8n", + "license": "SEE LICENSE IN LICENSE.md", + "homepage": "https://n8n.io", + "author": { + "name": "Jan Oberhauser", + "email": "jan@n8n.io" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/n8n-io/n8n.git" + }, + "main": "dist/src/index", + "types": "dist/src/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "npm run watch", + "tslint": "tslint -p tsconfig.json -c tslint.json", + "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", + "watch": "tsc --watch", + "test": "jest" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@types/cron": "^1.7.1", + "@types/crypto-js": "^4.0.1", + "@types/express": "^4.17.6", + "@types/jest": "^26.0.13", + "@types/lodash.get": "^4.4.6", + "@types/mime-types": "^2.1.0", + "@types/node": "14.0.27", + "@types/request-promise-native": "~1.0.15", + "jest": "^26.4.2", + "source-map-support": "^0.5.9", + "ts-jest": "^26.3.0", + "tslint": "^6.1.2", + "typescript": "~3.9.7" + }, + "dependencies": { + "client-oauth2": "^4.2.5", + "cron": "^1.7.2", + "crypto-js": "4.0.0", + "file-type": "^14.6.2", + "lodash.get": "^4.4.2", + "mime-types": "^2.1.27", + "n8n-workflow": "~0.55.0", + "oauth-1.0a": "^2.2.6", + "p-cancelable": "^2.0.0", + "request": "^2.88.2", + "request-promise-native": "^1.0.7" + }, + "jest": { + "transform": { + "^.+\\.tsx?$": "ts-jest" }, - "repository": { - "type": "git", - "url": "git+https://github.com/n8n-io/n8n.git" - }, - "main": "dist/src/index", - "types": "dist/src/index.d.ts", - "scripts": { - "build": "tsc", - "dev": "npm run watch", - "tslint": "tslint -p tsconfig.json -c tslint.json", - "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", - "watch": "tsc --watch", - "test": "jest" - }, - "files": [ - "dist" + "testURL": "http://localhost/", + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "testPathIgnorePatterns": [ + "/dist/", + "/node_modules/" ], - "devDependencies": { - "@types/cron": "^1.7.1", - "@types/crypto-js": "^4.0.1", - "@types/express": "^4.17.6", - "@types/jest": "^26.0.13", - "@types/lodash.get": "^4.4.6", - "@types/mime-types": "^2.1.0", - "@types/node": "14.0.27", - "@types/request-promise-native": "~1.0.15", - "jest": "^26.4.2", - "source-map-support": "^0.5.9", - "ts-jest": "^26.3.0", - "tslint": "^6.1.2", - "typescript": "~3.9.7" - }, - "dependencies": { - "client-oauth2": "^4.2.5", - "cron": "^1.7.2", - "crypto-js": "4.0.0", - "file-type": "^14.6.2", - "lodash.get": "^4.4.2", - "mime-types": "^2.1.27", - "n8n-workflow": "~0.51.0", - "oauth-1.0a": "^2.2.6", - "p-cancelable": "^2.0.0", - "request": "^2.88.2", - "request-promise-native": "^1.0.7" - }, - "jest": { - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testURL": "http://localhost/", - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "testPathIgnorePatterns": [ - "/dist/", - "/node_modules/" - ], - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json", - "node" - ] - } + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json", + "node" + ] + } } diff --git a/packages/core/src/ActiveWebhooks.ts b/packages/core/src/ActiveWebhooks.ts index 86a1c0f690..95b7f9dc3c 100644 --- a/packages/core/src/ActiveWebhooks.ts +++ b/packages/core/src/ActiveWebhooks.ts @@ -2,6 +2,7 @@ import { IWebhookData, WebhookHttpMethod, Workflow, + WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -30,7 +31,7 @@ export class ActiveWebhooks { * @returns {Promise} * @memberof ActiveWebhooks */ - async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode): Promise { + async add(workflow: Workflow, webhookData: IWebhookData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { if (workflow.id === undefined) { throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); } @@ -57,10 +58,10 @@ export class ActiveWebhooks { this.webhookUrls[webhookKey].push(webhookData); try { - const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); + const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks); if (webhookExists !== true) { // If webhook does not exist yet create it - await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); + await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, activation, this.testWebhooks); } } catch (error) { @@ -183,7 +184,7 @@ export class ActiveWebhooks { // Go through all the registered webhooks of the workflow and remove them for (const webhookData of webhooks) { - await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); + await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', this.testWebhooks); delete this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path, webhookData.webhookId)]; } diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 86a88c617b..320e9fa73f 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -8,6 +8,8 @@ import { ITriggerResponse, IWorkflowExecuteAdditionalData, Workflow, + WorkflowActivateMode, + WorkflowExecuteMode, } from 'n8n-workflow'; import { @@ -66,14 +68,14 @@ export class ActiveWorkflows { * @returns {Promise} * @memberof ActiveWorkflows */ - async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise { + async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise { this.workflowData[id] = {}; const triggerNodes = workflow.getTriggerNodes(); let triggerResponse: ITriggerResponse | undefined; this.workflowData[id].triggerResponses = []; for (const triggerNode of triggerNodes) { - triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, 'trigger'); + triggerResponse = await workflow.runTrigger(triggerNode, getTriggerFunctions, additionalData, mode, activation); if (triggerResponse !== undefined) { // If a response was given save it this.workflowData[id].triggerResponses!.push(triggerResponse); @@ -84,7 +86,7 @@ export class ActiveWorkflows { if (pollNodes.length) { this.workflowData[id].pollResponses = []; for (const pollNode of pollNodes) { - this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions)); + this.workflowData[id].pollResponses!.push(await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions, mode, activation)); } } } @@ -100,10 +102,8 @@ export class ActiveWorkflows { * @returns {Promise} * @memberof ActiveWorkflows */ - async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions): Promise { - const mode = 'trigger'; - - const pollFunctions = getPollFunctions(workflow, node, additionalData, mode); + async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { + const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation); const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as { item: ITriggerTime[]; diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index dcdc2bd4bb..2646fbb7b7 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -34,6 +34,7 @@ import { NodeHelpers, NodeParameterValue, Workflow, + WorkflowActivateMode, WorkflowDataProxy, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -103,6 +104,9 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m const filePathParts = path.parse(filePath as string); + if (filePathParts.dir !== '') { + returnData.directory = filePathParts.dir; + } returnData.fileName = filePathParts.base; // Remove the dot @@ -161,8 +165,9 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin return this.helpers.request!(newRequestOptions) .catch(async (error: IResponseError) => { - // TODO: Check if also other codes are possible - if (error.statusCode === 401) { + const statusCodeReturned = oAuth2Options?.tokenExpiredStatusCode === undefined ? 401 : oAuth2Options?.tokenExpiredStatusCode; + + if (error.statusCode === statusCodeReturned) { // Token is probably not valid anymore. So try refresh it. const tokenRefreshOptions: IDataObject = {}; @@ -531,7 +536,7 @@ export function getWorkflowMetadata(workflow: Workflow): IWorkflowMetadata { * @returns {ITriggerFunctions} */ // TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add -export function getExecutePollFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IPollFunctions { +export function getExecutePollFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IPollFunctions { return ((workflow: Workflow, node: INode) => { return { __emit: (data: INodeExecutionData[][]): void => { @@ -543,6 +548,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio getMode: (): WorkflowExecuteMode => { return mode; }, + getActivationMode: (): WorkflowActivateMode => { + return activation; + }, getNode: () => { return getNode(node); }, @@ -594,7 +602,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio * @returns {ITriggerFunctions} */ // TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add -export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions { +export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): ITriggerFunctions { return ((workflow: Workflow, node: INode) => { return { emit: (data: INodeExecutionData[][]): void => { @@ -609,6 +617,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi getMode: (): WorkflowExecuteMode => { return mode; }, + getActivationMode: (): WorkflowActivateMode => { + return activation; + }, getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any const runExecutionData: IRunExecutionData | null = null; const itemIndex = 0; @@ -906,7 +917,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio * @param {WorkflowExecuteMode} mode * @returns {IHookFunctions} */ -export function getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions { +export function getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions { return ((workflow: Workflow, node: INode) => { const that = { getCredentials(type: string): ICredentialDataDecryptedObject | undefined { @@ -915,6 +926,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio getMode: (): WorkflowExecuteMode => { return mode; }, + getActivationMode: (): WorkflowActivateMode => { + return activation; + }, getNode: () => { return getNode(node); }, diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 07be9fbfda..a71bc3bfa3 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -22,6 +22,8 @@ import { NodeExecuteFunctions, } from './'; +import { get } from 'lodash'; + export class WorkflowExecute { runExecutionData: IRunExecutionData; private additionalData: IWorkflowExecuteAdditionalData; @@ -234,6 +236,21 @@ export class WorkflowExecute { } + /** + * Checks the incoming connection does not receive any data + */ + incomingConnectionIsEmpty(runData: IRunData, inputConnections: IConnection[], runIndex: number): boolean { + // for (const inputConnection of workflow.connectionsByDestinationNode[nodeToAdd].main[0]) { + for (const inputConnection of inputConnections) { + const nodeIncomingData = get(runData, `[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`); + if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) { + return false; + } + } + return true; + } + + addNodeToBeExecuted(workflow: Workflow, connectionData: IConnection, outputIndex: number, parentNodeName: string, nodeSuccessData: INodeExecutionData[][], runIndex: number): void { let stillDataMissing = false; @@ -299,7 +316,7 @@ export class WorkflowExecute { if (nodeWasWaiting === false) { - // Get a list of all the output nodes that we can check for siblings eaiser + // Get a list of all the output nodes that we can check for siblings easier const checkOutputNodes = []; for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) { if (!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)) { @@ -327,8 +344,12 @@ export class WorkflowExecute { // previously processed one if (inputData.node !== parentNodeName && checkOutputNodes.includes(inputData.node)) { // So the parent node will be added anyway which - // will then process this node next. So nothing to do. - continue; + // will then process this node next. So nothing to do + // unless the incoming data of the node is empty + // because then it would not be executed + if (!this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[inputData.node].main[0], runIndex)) { + continue; + } } // Check if it is already in the execution stack @@ -384,7 +405,19 @@ export class WorkflowExecute { continue; } - if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) { + let addEmptyItem = false; + + if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) { + // Add empty item if the node does not have any input connections + addEmptyItem = true; + } else { + if (this.incomingConnectionIsEmpty(this.runExecutionData.resultData.runData, workflow.connectionsByDestinationNode[nodeToAdd].main[0], runIndex)) { + // Add empty item also if the input data is empty + addEmptyItem = true; + } + } + + if (addEmptyItem === true) { // Add only node if it does not have any inputs because else it will // be added by its input node later anyway. this.runExecutionData.executionData!.nodeExecutionStack.push( @@ -524,7 +557,7 @@ export class WorkflowExecute { executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; executionNode = executionData.node; - this.executeHook('nodeExecuteBefore', [executionNode.name]); + await this.executeHook('nodeExecuteBefore', [executionNode.name]); // Get the index of the current run runIndex = 0; @@ -689,7 +722,7 @@ export class WorkflowExecute { // Add the execution data again so that it can get restarted this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData); - this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); + await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]); break; } diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index ed9897a771..4335e9e1e9 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -596,7 +596,7 @@ class NodeTypesClass implements INodeTypes { let item: INodeExecutionData; let keepOnlySet: boolean; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, []) as boolean; + keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean; item = items[itemIndex]; const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts index 1136d497ac..c89cc07fd8 100644 --- a/packages/core/test/WorkflowExecute.test.ts +++ b/packages/core/test/WorkflowExecute.test.ts @@ -986,6 +986,172 @@ describe('WorkflowExecute', () => { }, }, }, + { + description: 'should use empty data if input of sibling does not receive any data from parent', + input: { + // Leave the workflowData in regular JSON to be able to easily + // copy it from/in the UI + workflowData: { + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.start", + "typeVersion": 1, + "position": [ + 250, + 300, + ], + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{$json[\"value1\"]}}", + "operation": "equal", + "value2": 1, + }, + ], + }, + }, + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 650, + 300, + ], + }, + { + "parameters": { + "values": { + "string": [], + "number": [ + { + "name": "value2", + "value": 2, + }, + ], + }, + "options": {}, + }, + "name": "Set2", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 850, + 450, + ], + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "value1", + "value": 1, + }, + ], + }, + "options": {}, + }, + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 450, + 300, + ], + }, + { + "parameters": {}, + "name": "Merge", + "type": "n8n-nodes-base.merge", + "typeVersion": 1, + "position": [ + 1050, + 300, + ], + }, + ], + "connections": { + "Start": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0, + }, + ], + ], + }, + "IF": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0, + }, + ], + [ + { + "node": "Set2", + "type": "main", + "index": 0, + }, + ], + ], + }, + "Set2": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 1, + }, + ], + ], + }, + "Set1": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: [ + 'Start', + 'Set1', + 'IF', + 'Set2', + 'Merge', + ], + nodeData: { + Merge: [ + [ + { + value1: 1, + }, + { + value2: 2, + }, + ], + ], + }, + }, + }, ]; diff --git a/packages/editor-ui/.editorconfig b/packages/editor-ui/.editorconfig index bec7553240..9e24325d41 100644 --- a/packages/editor-ui/.editorconfig +++ b/packages/editor-ui/.editorconfig @@ -6,3 +6,10 @@ indent_style = tab end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + +[package.json] +indent_style = space +indent_size = 2 + +[*.ts] +quote_type = single diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index f7b1cfe51f..806d31c247 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.77.0", + "version": "0.84.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -65,7 +65,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.51.0", + "n8n-workflow": "~0.55.0", "node-sass": "^4.12.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index b4a5e248ca..972ef0cbd8 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -314,8 +314,7 @@ export interface IExecutionsListResponse { } export interface IExecutionsCurrentSummaryExtended { - id?: string; - idActive: string; + id: string; finished?: boolean; mode: WorkflowExecuteMode; retryOf?: string; @@ -334,8 +333,7 @@ export interface IExecutionsStopData { } export interface IExecutionsSummary { - id?: string; // executionIdDb - idActive?: string; // executionIdActive + id: string; mode: WorkflowExecuteMode; finished?: boolean; retryOf?: string; @@ -370,8 +368,7 @@ export interface IPushDataExecutionStarted { export interface IPushDataExecutionFinished { data: IRun; - executionIdActive: string; - executionIdDb?: string; + executionId: string; retryOf?: string; } diff --git a/packages/editor-ui/src/components/CodeEdit.vue b/packages/editor-ui/src/components/CodeEdit.vue index d89e820b09..fb1c02521d 100644 --- a/packages/editor-ui/src/components/CodeEdit.vue +++ b/packages/editor-ui/src/components/CodeEdit.vue @@ -6,7 +6,7 @@ {{parameter.displayName}}:
- +
@@ -14,42 +14,43 @@ cortex-logo \ No newline at end of file diff --git a/packages/nodes-base/nodes/DeepL/DeepL.node.ts b/packages/nodes-base/nodes/DeepL/DeepL.node.ts new file mode 100644 index 0000000000..2d82434f69 --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/DeepL.node.ts @@ -0,0 +1,131 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + deepLApiRequest, +} from './GenericFunctions'; + +import { + textOperations +} from './TextDescription'; + +export class DeepL implements INodeType { + description: INodeTypeDescription = { + displayName: 'DeepL', + name: 'deepL', + icon: 'file:deepl.svg', + group: ['input', 'output'], + version: 1, + description: 'Translate data using DeepL', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'DeepL', + color: '#0f2b46', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'deepLApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Language', + value: 'language', + }, + ], + default: 'language', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'language', + ], + }, + }, + options: [ + { + name: 'Translate', + value: 'translate', + description: 'Translate data', + }, + ], + default: 'translate', + description: 'The operation to perform', + }, + ...textOperations, + ], + }; + + methods = { + loadOptions: { + async getLanguages(this: ILoadOptionsFunctions) { + const returnData: INodePropertyOptions[] = []; + const languages = await deepLApiRequest.call(this, 'GET', '/languages', {}, { type: 'target' }); + for (const language of languages) { + returnData.push({ + name: language.name, + value: language.language, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = items.length; + + const responseData = []; + + for (let i = 0; i < length; i++) { + + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (resource === 'language') { + + if (operation === 'translate') { + + const text = this.getNodeParameter('text', i) as string; + const translateTo = this.getNodeParameter('translateTo', i) as string; + const qs = { target_lang: translateTo, text } as IDataObject; + + if (additionalFields.sourceLang !== undefined) { + qs.source_lang = ['EN-GB', 'EN-US'].includes(additionalFields.sourceLang as string) + ? 'EN' + : additionalFields.sourceLang; + } + + const response = await deepLApiRequest.call(this, 'GET', '/translate', {}, qs); + responseData.push(response.translations[0]); + } + } + } + + return [this.helpers.returnJsonArray(responseData)]; + } +} diff --git a/packages/nodes-base/nodes/DeepL/GenericFunctions.ts b/packages/nodes-base/nodes/DeepL/GenericFunctions.ts new file mode 100644 index 0000000000..7b8f9a1004 --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/GenericFunctions.ts @@ -0,0 +1,62 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function deepLApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, +) { + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.deepl.com/v2${resource}`, + json: true, + }; + + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + const credentials = this.getCredentials('deepLApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + options.qs.auth_key = credentials.apiKey; + + return await this.helpers.request!(options); + + } catch (error) { + if (error?.response?.body?.message) { + // Try to return the error prettier + throw new Error(`DeepL error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/DeepL/TextDescription.ts b/packages/nodes-base/nodes/DeepL/TextDescription.ts new file mode 100644 index 0000000000..7b2d1d5f1f --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/TextDescription.ts @@ -0,0 +1,125 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const textOperations = [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Input text to translate.', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + operation: [ + 'translate', + ], + }, + }, + }, + { + displayName: 'Target Language', + name: 'translateTo', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + default: '', + description: 'Language to translate to.', + required: true, + displayOptions: { + show: { + operation: [ + 'translate', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Source Language', + name: 'sourceLang', + type: 'options', + default: '', + description: 'Language to translate from.', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + }, + { + displayName: 'Split Sentences', + name: 'splitSentences', + type: 'options', + default: '1', + description: 'How the translation engine should split sentences.', + options: [ + { + name: 'Interpunction Only', + value: 'nonewlines', + description: 'Split text on interpunction only, ignoring newlines.', + }, + { + name: 'No Splitting', + value: '0', + description: 'Treat all text as a single sentence.', + }, + { + name: 'On Punctuation and Newlines', + value: '1', + description: 'Split text on interpunction and newlines.', + }, + ], + }, + { + displayName: 'Preserve Formatting', + name: 'preserveFormatting', + type: 'options', + default: '0', + description: 'Whether the translation engine should respect the original formatting, even if it would usually correct some aspects.', + options: [ + { + name: 'Apply corrections', + value: '0', + description: 'Fix punctuation at the beginning and end of sentences and fixes lower/upper caseing at the beginning.', + }, + { + name: 'Do not correct', + value: '1', + description: 'Keep text as similar as possible to the original.', + }, + ], + }, + { + displayName: 'Formality', + name: 'formality', + type: 'options', + default: 'default', + description: 'How formal or informal the target text should be. May not be supported with all languages.', + options: [ + { + name: 'Formal', + value: 'more', + }, + { + name: 'Informal', + value: 'less', + }, + { + name: 'Neutral', + value: 'default', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/DeepL/deepl.svg b/packages/nodes-base/nodes/DeepL/deepl.svg new file mode 100644 index 0000000000..706dacab12 --- /dev/null +++ b/packages/nodes-base/nodes/DeepL/deepl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Demio/Demio.node.json b/packages/nodes-base/nodes/Demio/Demio.node.json new file mode 100644 index 0000000000..8259450ed4 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/Demio.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.demio", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/demio" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.demio/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Demio/Demio.node.ts b/packages/nodes-base/nodes/Demio/Demio.node.ts new file mode 100644 index 0000000000..f0b21b483d --- /dev/null +++ b/packages/nodes-base/nodes/Demio/Demio.node.ts @@ -0,0 +1,211 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + demioApiRequest, +} from './GenericFunctions'; + +import { + eventFields, + eventOperations, +} from './EventDescription'; + +import { + reportFields, + reportOperations, +} from './ReportDescription'; + +export class Demio implements INodeType { + description: INodeTypeDescription = { + displayName: 'Demio', + name: 'demio', + icon: 'file:demio.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Demio API', + defaults: { + name: 'Demio', + color: '#02bf6f', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'demioApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Event', + value: 'event', + }, + { + name: 'Report', + value: 'report', + }, + ], + default: 'event', + description: 'Resource to consume.', + }, + // Event + ...eventOperations, + ...eventFields, + // Report + ...reportOperations, + ...reportFields, + ], + }; + + methods = { + loadOptions: { + // Get all the events to display them to user so that he can + // select them easily + async getEvents( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const events = await demioApiRequest.call( + this, + 'GET', + `/events`, + {}, + { type: 'upcoming' }, + ); + for (const event of events) { + returnData.push({ + name: event.name, + value: event.id, + }); + } + return returnData; + }, + + // Get all the sessions to display them to user so that he can + // select them easily + async getEventSessions( + this: ILoadOptionsFunctions, + ): Promise { + const eventId = this.getCurrentNodeParameter('eventId') as string; + const qs: IDataObject = {}; + + const resource = this.getCurrentNodeParameter('resource') as string; + + if (resource !== 'report') { + qs.active = true; + } + + const returnData: INodePropertyOptions[] = []; + const { dates } = await demioApiRequest.call( + this, + 'GET', + `/event/${eventId}`, + {}, + ); + for (const date of dates) { + returnData.push({ + name: date.datetime, + value: date.date_id, + }); + } + return returnData; + }, + }, + }; + + 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; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + if (resource === 'event') { + if (operation === 'get') { + const id = this.getNodeParameter('eventId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.date_id !== undefined) { + responseData = await demioApiRequest.call(this, 'GET', `/event/${id}/date/${additionalFields.date_id}`); + } else { + Object.assign(qs, additionalFields); + responseData = await demioApiRequest.call(this, 'GET', `/event/${id}`, {}, qs); + } + } + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + Object.assign(qs, filters); + + responseData = await demioApiRequest.call(this, 'GET', `/events`, {}, qs); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + + } + if (operation === 'register') { + const eventId = this.getNodeParameter('eventId', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const email = this.getNodeParameter('email', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name: firstName, + email, + id: eventId, + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.value }), {}); + Object.assign(body, data); + delete additionalFields.customFields; + } + + responseData = await demioApiRequest.call(this, 'PUT', `/event/register`, body); + } + } + if (resource === 'report') { + if (operation === 'get') { + const sessionId = this.getNodeParameter('dateId', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + Object.assign(qs, filters); + + responseData = await demioApiRequest.call(this, 'GET', `/report/${sessionId}/participants`, {}, qs); + responseData = responseData.participants; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Demio/EventDescription.ts b/packages/nodes-base/nodes/Demio/EventDescription.ts new file mode 100644 index 0000000000..882ec45dad --- /dev/null +++ b/packages/nodes-base/nodes/Demio/EventDescription.ts @@ -0,0 +1,345 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an event', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all events', + }, + { + name: 'Register', + value: 'register', + description: 'Register someone to an event', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + + /* -------------------------------------------------------------------------- */ + /* event:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Automated', + value: 'automated', + }, + { + name: 'Past', + value: 'past', + }, + { + name: 'Upcoming', + value: 'upcoming', + }, + ], + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* event:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + default: '', + required: true, + description: 'Event ID', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + description: 'Return only active dates in series', + }, + { + displayName: 'Session ID', + name: 'date_id', + type: 'string', + default: '', + description: 'Event Date ID', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* event:register */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + default: '', + description: 'Event ID', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + required: true, + description: 'The registrant\'s first name', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + description: 'The registrant\'s email address', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + options: [ + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'The value for the predefined Company field.', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'string', + default: '', + description: 'Each custom field\'s unique identifier
can be found within the Event\'s Registration block in the Customize tab.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Event Registration URL', + name: 'ref_url', + type: 'string', + default: '', + description: 'Event Registration page URL. It can be useful when you
do not know Event ID, but have Event link.', + }, + { + displayName: 'GDPR', + name: 'gdpr', + type: 'string', + default: '', + description: 'The value for the predefined GDPR field.', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'The value for the predefined Last Name field.', + }, + { + displayName: 'Phone Number', + name: 'phone_number', + type: 'string', + default: '', + description: 'The value for the predefined Phone Number field.', + }, + { + displayName: 'Session ID', + name: 'date_id', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventSessions', + loadOptionsDependsOn: [ + 'eventId', + ], + }, + default: '', + description: 'Event Session ID', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The value for the predefined Website field.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Demio/GenericFunctions.ts b/packages/nodes-base/nodes/Demio/GenericFunctions.ts new file mode 100644 index 0000000000..3d96faacca --- /dev/null +++ b/packages/nodes-base/nodes/Demio/GenericFunctions.ts @@ -0,0 +1,48 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function demioApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + try { + const credentials = this.getCredentials('demioApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + 'Api-Key': credentials.apiKey, + 'Api-Secret': credentials.apiSecret, + }, + method, + qs, + body, + uri: uri || `https://my.demio.com/api/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + + return await this.helpers.request!(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + const errorBody = error.response.body; + throw new Error(`Demio error response [${error.statusCode}]: ${errorBody.message}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Demio/ReportDescription.ts b/packages/nodes-base/nodes/Demio/ReportDescription.ts new file mode 100644 index 0000000000..b83982f220 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/ReportDescription.ts @@ -0,0 +1,124 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const reportOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'report', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an event report', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const reportFields = [ + + /* -------------------------------------------------------------------------- */ + /* report:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Event ID', + }, + { + displayName: 'Session ID', + name: 'dateId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventSessions', + loadOptionsDependsOn: [ + 'eventId', + ], + }, + default: '', + required: true, + description: 'ID of the session', + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Attended', + value: 'attended', + }, + { + name: 'Banned', + value: 'banned', + }, + { + name: 'Completed', + value: 'completed', + }, + { + name: 'Did Not Attend', + value: 'did-not-attend', + }, + { + name: 'Left Early', + value: 'left-early', + }, + ], + default: '', + description: 'Filter results by participation status', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Demio/demio.svg b/packages/nodes-base/nodes/Demio/demio.svg new file mode 100644 index 0000000000..93889f1d02 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/demio.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts index 1c7efac402..4421be2d4f 100644 --- a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts +++ b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts @@ -11,21 +11,25 @@ import { } from 'n8n-workflow'; import { - dropboxApiRequest + dropboxApiRequest, + dropboxpiRequestAllItems, + getCredentials, + getRootDirectory, + simplify, } from './GenericFunctions'; export class Dropbox implements INodeType { description: INodeTypeDescription = { displayName: 'Dropbox', name: 'dropbox', - icon: 'file:dropbox.png', + icon: 'file:dropbox.svg', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Access data on Dropbox', defaults: { name: 'Dropbox', - color: '#0062ff', + color: '#007ee5', }, inputs: ['main'], outputs: ['main'], @@ -84,6 +88,10 @@ export class Dropbox implements INodeType { name: 'Folder', value: 'folder', }, + { + name: 'Search', + value: 'search', + }, ], default: 'file', description: 'The resource to operate on.', @@ -176,6 +184,27 @@ export class Dropbox implements INodeType { description: 'The operation to perform.', }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'search', + ], + }, + }, + options: [ + { + name: 'Query', + value: 'query', + }, + ], + default: 'query', + description: 'The operation to perform.', + }, + // ---------------------------------- // file // ---------------------------------- @@ -419,7 +448,189 @@ export class Dropbox implements INodeType { description: 'Name of the binary property which contains
the data for the file to be uploaded.', }, - + // ---------------------------------- + // search:query + // ---------------------------------- + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + description: ' The string to search for. May match across multiple fields based on the request arguments.', + }, + { + displayName: 'File Status', + name: 'fileStatus', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Deleted', + value: 'deleted', + }, + ], + default: 'active', + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + description: ' The string to search for. May match across multiple fields based on the request arguments.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + 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: [ + 'search', + ], + operation: [ + 'query', + ], + returnAll: [ + false, + ], + }, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + }, + }, + options: [ + { + displayName: 'File Categories', + name: 'file_categories', + type: 'multiOptions', + options: [ + { + name: 'Audio (mp3, wav, mid, etc.)', + value: 'audio', + }, + { + name: 'Document (doc, docx, txt, etc.)', + value: 'document', + }, + { + name: 'Folder', + value: 'folder', + }, + { + name: 'Image (jpg, png, gif, etc.)', + value: 'image', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Dropbox Paper', + value: 'paper', + }, + { + name: 'PDF', + value: 'pdf', + }, + { + name: 'Presentation (ppt, pptx, key, etc.)', + value: 'presentation', + }, + { + name: 'Spreadsheet (xlsx, xls, csv, etc.)', + value: 'spreadsheet', + }, + { + name: 'Video (avi, wmv, mp4, etc.)', + value: 'video', + }, + ], + default: [], + }, + { + displayName: 'File Extensions', + name: 'file_extensions', + type: 'string', + default: '', + description: 'Multiple can be set separated by comma. Example: jpg,pdf', + }, + { + displayName: 'Folder', + name: 'path', + type: 'string', + default: '', + description: 'If this field is not specified, this module searches the entire Dropbox', + }, + ], + }, // ---------------------------------- // folder @@ -469,7 +680,97 @@ export class Dropbox implements INodeType { placeholder: '/invoices/2019/', description: 'The path of which to list the content.', }, - + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'list', + ], + resource: [ + 'folder', + ], + }, + }, + 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: [ + 'folder', + ], + operation: [ + 'list', + ], + returnAll: [ + false, + ], + }, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'list', + ], + }, + }, + options: [ + { + displayName: 'Include Deleted', + name: 'include_deleted', + type: 'boolean', + default: false, + description: 'If true, the results will include entries for files and folders that used to exist but were deleted. The default for this field is False.', + }, + { + displayName: 'Include Shared Members ', + name: 'include_has_explicit_shared_members', + type: 'boolean', + default: false, + description: 'If true, the results will include a flag for each file indicating whether or not that file has any explicit members. The default for this field is False.', + }, + { + displayName: 'Include Mounted Folders ', + name: 'include_mounted_folders', + type: 'boolean', + default: true, + description: 'If true, the results will include entries under mounted folders which includes app folder, shared folder and team folder. The default for this field is True.', + }, + { + displayName: 'Include Non Downloadable Files ', + name: 'include_non_downloadable_files', + type: 'boolean', + default: true, + description: 'If true, include files that are not downloadable, i.e. Google Docs. The default for this field is True.', + }, + { + displayName: 'Recursive', + name: 'recursive', + type: 'boolean', + default: false, + description: 'If true, the list folder operation will be applied recursively to all subfolders and the response will contain contents of all subfolders. The default for this field is False.', + }, + ], + }, ], }; @@ -484,11 +785,29 @@ export class Dropbox implements INodeType { let endpoint = ''; let requestMethod = ''; + let returnAll = false; + let property = ''; let body: IDataObject | Buffer; let options; const query: IDataObject = {}; - const headers: IDataObject = {}; + let headers: IDataObject = {}; + let simple = false; + + + const { accessType } = getCredentials.call(this); + + if (accessType === 'full') { + // get the root directory to set it as the default for all operations + const { root_info: { root_namespace_id } } = await getRootDirectory.call(this); + + headers = { + 'dropbox-api-path-root': JSON.stringify({ + '.tag': 'root', + 'root': root_namespace_id, + }), + }; + } for (let i = 0; i < items.length; i++) { body = {}; @@ -522,9 +841,9 @@ export class Dropbox implements INodeType { endpoint = 'https://content.dropboxapi.com/2/files/upload'; - if (this.getNodeParameter('binaryData', i) === true) { + options = { json: false }; - options = { json: false }; + if (this.getNodeParameter('binaryData', i) === true) { // Is binary file to upload const item = items[i]; @@ -545,7 +864,6 @@ export class Dropbox implements INodeType { body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); } } - } else if (resource === 'folder') { if (operation === 'create') { // ---------------------------------- @@ -564,20 +882,65 @@ export class Dropbox implements INodeType { // list // ---------------------------------- + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + property = 'entries'; + requestMethod = 'POST'; body = { path: this.getNodeParameter('path', i) as string, - limit: 2000, + limit: 1000, }; - // TODO: If more files than the max-amount exist it has to be possible to - // also request them. + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + body.limit = limit; + } + + Object.assign(body, filters); endpoint = 'https://api.dropboxapi.com/2/files/list_folder'; } + } else if (resource === 'search') { + if (operation === 'query') { + // ---------------------------------- + // query + // ---------------------------------- + + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + simple = this.getNodeParameter('simple', 0) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + property = 'matches'; + + requestMethod = 'POST'; + body = { + query: this.getNodeParameter('query', i) as string, + options: { + filename_only: true, + }, + }; + + if (filters.file_extensions) { + filters.file_extensions = (filters.file_extensions as string).split(','); + } + + Object.assign(body.options, filters); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + Object.assign(body.options, { max_results: limit }); + } + + endpoint = 'https://api.dropboxapi.com/2/files/search_v2'; + } } - if (['file', 'folder'].includes(resource)) { + if (['file', 'folder', 'search'].includes(resource)) { if (operation === 'copy') { // ---------------------------------- // copy @@ -625,7 +988,13 @@ export class Dropbox implements INodeType { options = { encoding: null }; } - let responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options); + let responseData; + + if (returnAll === true) { + responseData = await dropboxpiRequestAllItems.call(this, property, requestMethod, endpoint, body, query, headers); + } else { + responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options); + } if (resource === 'file' && operation === 'upload') { responseData = JSON.parse(responseData); @@ -665,7 +1034,11 @@ export class Dropbox implements INodeType { 'content_hash': 'contentHash', }; - for (const item of responseData.entries) { + if (returnAll === false) { + responseData = responseData.entries; + } + + for (const item of responseData) { const newItem: IDataObject = {}; // Get the props and save them under a proper name @@ -677,8 +1050,14 @@ export class Dropbox implements INodeType { returnData.push(newItem as IDataObject); } + } else if (resource === 'search' && operation === 'query') { + if (returnAll === true) { + returnData.push.apply(returnData, (simple === true) ? simplify(responseData) : responseData); + } else { + returnData.push.apply(returnData, (simple === true) ? simplify(responseData[property]) : responseData[property]); + } } else { - returnData.push(responseData as IDataObject); + returnData.push(responseData); } } diff --git a/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts b/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts index 880ec6bc8c..ca7e4829bb 100644 --- a/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts @@ -20,7 +20,7 @@ import { * @param {object} body * @returns {Promise} */ -export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers?: object, option: IDataObject = {}): Promise {// tslint:disable-line:no-any +export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers: object = {}, option: IDataObject = {}): Promise {// tslint:disable-line:no-any const options: OptionsWithUri = { headers, @@ -67,3 +67,59 @@ export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions throw error; } } + +export async function dropboxpiRequestAllItems(this: IExecuteFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const resource = this.getNodeParameter('resource', 0) as string; + + const returnData: IDataObject[] = []; + + const paginationEndpoint: IDataObject = { + 'folder': 'https://api.dropboxapi.com/2/files/list_folder/continue', + 'search': 'https://api.dropboxapi.com/2/files/search/continue_v2', + }; + + let responseData; + do { + responseData = await dropboxApiRequest.call(this, method, endpoint, body, query, headers); + const cursor = responseData.cursor; + if (cursor !== undefined) { + endpoint = paginationEndpoint[resource] as string; + body = { cursor }; + } + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.has_more !== false + ); + + return returnData; +} + +export function getRootDirectory(this: IHookFunctions | IExecuteFunctions) { + return dropboxApiRequest.call(this, 'POST', 'https://api.dropboxapi.com/2/users/get_current_account', {}); +} + +export function simplify(data: IDataObject[]) { + const results = []; + for (const element of data) { + const { '.tag': key } = element?.metadata as IDataObject; + const metadata = (element?.metadata as IDataObject)[key as string] as IDataObject; + delete element.metadata; + Object.assign(element, metadata); + if ((element?.match_type as IDataObject)['.tag']) { + element.match_type = (element?.match_type as IDataObject)['.tag'] as string; + } + results.push(element); + } + return results; +} + +export function getCredentials(this: IExecuteFunctions) { + const authenticationMethod = this.getNodeParameter('authentication', 0) as string; + if (authenticationMethod === 'accessToken') { + return this.getCredentials('dropboxApi') as IDataObject; + } else { + return this.getCredentials('dropboxOAuth2Api') as IDataObject; + } +} + diff --git a/packages/nodes-base/nodes/Dropbox/dropbox.png b/packages/nodes-base/nodes/Dropbox/dropbox.png deleted file mode 100644 index 6fe6d86751..0000000000 Binary files a/packages/nodes-base/nodes/Dropbox/dropbox.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Dropbox/dropbox.svg b/packages/nodes-base/nodes/Dropbox/dropbox.svg new file mode 100644 index 0000000000..d6896df517 --- /dev/null +++ b/packages/nodes-base/nodes/Dropbox/dropbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/ERPNext/DocumentDescription.ts b/packages/nodes-base/nodes/ERPNext/DocumentDescription.ts new file mode 100644 index 0000000000..775c658cc9 --- /dev/null +++ b/packages/nodes-base/nodes/ERPNext/DocumentDescription.ts @@ -0,0 +1,463 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const documentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'document', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a document.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a document.', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a document.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all documents.', + }, + { + name: 'Update', + value: 'update', + description: 'Update a document.', + }, + ], + default: 'create', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const documentFields = [ + // ---------------------------------- + // document: getAll + // ---------------------------------- + { + displayName: 'DocType', + name: 'docType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocTypes', + }, + default: '', + description: 'DocType whose documents to retrieve.', + placeholder: 'Customer', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all items.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getDocFilters', + loadOptionsDependsOn: [ + 'docType', + ], + }, + default: '', + description: 'Comma-separated list of fields to return.', + placeholder: 'name,country', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'fixedCollection', + placeholder: 'Add Filter', + description: 'Custom Properties', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'customProperty', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocFields', + loadOptionsDependsOn: [ + 'docType', + ], + }, + default: '', + }, + { + displayName: 'Operator', + name: 'operator', + type: 'options', + default: 'is', + options: [ + { + name: 'IS', + value: 'is', + }, + { + name: 'IS NOT', + value: 'isNot', + }, + { + name: 'IS GREATER', + value: 'greater', + }, + { + name: 'IS LESS', + value: 'less', + }, + { + name: 'EQUALS, or GREATER', + value: 'equalsGreater', + }, + { + name: 'EQUALS, or LESS', + value: 'equalsLess', + }, + ], + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the operator condition.', + }, + ], + }, + ], + }, + ], + }, + + // ---------------------------------- + // document: create + // ---------------------------------- + { + displayName: 'DocType', + name: 'docType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDocTypes', + }, + required: true, + description: 'DocType you would like to create.', + placeholder: 'Customer', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Properties', + name: 'properties', + type: 'fixedCollection', + placeholder: 'Add Property', + required: true, + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Property', + name: 'customProperty', + placeholder: 'Add Property', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocFields', + loadOptionsDependsOn: [ + 'docType', + ], + }, + default: [], + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + + // ---------------------------------- + // document: get + // ---------------------------------- + { + displayName: 'DocType', + name: 'docType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocTypes', + }, + default: '', + description: 'The type of document you would like to get.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'get', + ], + }, + }, + required: true, + }, + { + displayName: 'Document Name', + name: 'documentName', + type: 'string', + default: '', + description: 'The name (ID) of document you would like to get.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'get', + ], + }, + }, + required: true, + }, + + // ---------------------------------- + // document: delete + // ---------------------------------- + { + displayName: 'DocType', + name: 'docType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocTypes', + }, + default: '', + description: 'The type of document you would like to delete.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'delete', + ], + }, + }, + required: true, + }, + { + displayName: 'Document Name', + name: 'documentName', + type: 'string', + default: '', + description: 'The name (ID) of document you would like to get.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'delete', + ], + }, + }, + required: true, + }, + + // ---------------------------------- + // document: update + // ---------------------------------- + { + displayName: 'DocType', + name: 'docType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocTypes', + }, + default: '', + description: 'The type of document you would like to update', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + }, + { + displayName: 'Document Name', + name: 'documentName', + type: 'string', + default: '', + description: 'The name (ID) of document you would like to get.', + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + }, + }, + required: true, + }, + { + displayName: 'Properties', + name: 'properties', + type: 'fixedCollection', + placeholder: 'Add Property', + description: 'Properties of request body.', + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Property', + name: 'customProperty', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDocFields', + loadOptionsDependsOn: [ + 'docType', + ], + }, + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ERPNext/ERPNext.node.ts b/packages/nodes-base/nodes/ERPNext/ERPNext.node.ts new file mode 100644 index 0000000000..b5fc28a8bb --- /dev/null +++ b/packages/nodes-base/nodes/ERPNext/ERPNext.node.ts @@ -0,0 +1,268 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + documentFields, + documentOperations, +} from './DocumentDescription'; + +import { + erpNextApiRequest, + erpNextApiRequestAllItems +} from './GenericFunctions'; + +import { + DocumentProperties, + processNames, + toSQL, +} from './utils'; + +export class ERPNext implements INodeType { + description: INodeTypeDescription = { + displayName: 'ERPNext', + name: 'erpNext', + icon: 'file:erpnext.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume ERPNext API', + defaults: { + name: 'ERPNext', + color: '#7574ff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'erpNextApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Document', + value: 'document', + }, + ], + default: 'document', + description: 'Resource to consume.', + }, + ...documentOperations, + ...documentFields, + ], + }; + + methods = { + loadOptions: { + async getDocTypes(this: ILoadOptionsFunctions): Promise { + const data = await erpNextApiRequestAllItems.call(this, 'data', 'GET', '/api/resource/DocType', {}); + const docTypes = data.map(({ name }: { name: string }) => { + return { name, value: encodeURI(name) }; + }); + + return processNames(docTypes); + }, + async getDocFilters(this: ILoadOptionsFunctions): Promise { + const docType = this.getCurrentNodeParameter('docType') as string; + const { data } = await erpNextApiRequest.call(this, 'GET', `/api/resource/DocType/${docType}`, {}); + + const docFields = data.fields.map(({ label, fieldname }: { label: string, fieldname: string }) => { + return ({ name: label, value: fieldname }); + }); + + docFields.unshift({ name: '*', value: '*' }); + + return processNames(docFields); + }, + async getDocFields(this: ILoadOptionsFunctions): Promise { + const docType = this.getCurrentNodeParameter('docType') as string; + const { data } = await erpNextApiRequest.call(this, 'GET', `/api/resource/DocType/${docType}`, {}); + + const docFields = data.fields.map(({ label, fieldname }: { label: string, fieldname: string }) => { + return ({ name: label, value: fieldname }); + }); + + return processNames(docFields); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const returnData: IDataObject[] = []; + let responseData; + + const body: IDataObject = {}; + const qs: IDataObject = {}; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < items.length; i++) { + + // https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/Resources/post_api_resource_Webhook + // https://frappeframework.com/docs/user/en/guides/integration/rest_api/manipulating_documents + + if (resource === 'document') { + + // ********************************************************************* + // document + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // document: get + // ---------------------------------- + + // https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/get_api_resource__DocType___DocumentName_ + + const docType = this.getNodeParameter('docType', i) as string; + const documentName = this.getNodeParameter('documentName', i) as string; + + responseData = await erpNextApiRequest.call(this, 'GET', `/api/resource/${docType}/${documentName}`); + responseData = responseData.data; + } + + if (operation === 'getAll') { + + // ---------------------------------- + // document: getAll + // ---------------------------------- + + // https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/get_api_resource__DocType_ + + const docType = this.getNodeParameter('docType', i) as string; + const endpoint = `/api/resource/${docType}`; + + const { + fields, + filters, + } = this.getNodeParameter('options', i) as { + fields: string[], + filters: { + customProperty: Array<{ field: string, operator: string, value: string }>, + }, + }; + + // fields=["test", "example", "hi"] + if (fields) { + if (fields.includes('*')) { + qs.fields = JSON.stringify(['*']); + } else { + qs.fields = JSON.stringify(fields); + } + } + // filters=[["Person","first_name","=","Jane"]] + // TODO: filters not working + if (filters) { + qs.filters = JSON.stringify(filters.customProperty.map((filter) => { + return [ + docType, + filter.field, + toSQL(filter.operator), + filter.value, + ]; + })); + } + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + qs.limit_page_length = limit; + qs.limit_start = 0; + responseData = await erpNextApiRequest.call(this, 'GET', endpoint, {}, qs); + responseData = responseData.data; + + } else { + responseData = await erpNextApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs); + } + + } else if (operation === 'create') { + + // ---------------------------------- + // document: create + // ---------------------------------- + + // https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/post_api_resource__DocType_ + + const properties = this.getNodeParameter('properties', i) as DocumentProperties; + + if (!properties.customProperty.length) { + throw new Error('Please enter at least one property for the document to create.'); + } + + properties.customProperty.forEach(property => { + body[property.field] = property.value; + }); + + const docType = this.getNodeParameter('docType', i) as string; + + responseData = await erpNextApiRequest.call(this, 'POST', `/api/resource/${docType}`, body); + responseData = responseData.data; + + } else if (operation === 'delete') { + + // ---------------------------------- + // document: delete + // ---------------------------------- + + // https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/delete_api_resource__DocType___DocumentName_ + + const docType = this.getNodeParameter('docType', i) as string; + const documentName = this.getNodeParameter('documentName', i) as string; + + responseData = await erpNextApiRequest.call(this, 'DELETE', `/api/resource/${docType}/${documentName}`); + + } else if (operation === 'update') { + + // ---------------------------------- + // document: update + // ---------------------------------- + + // https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/put_api_resource__DocType___DocumentName_ + + const properties = this.getNodeParameter('properties', i) as DocumentProperties; + + if (!properties.customProperty.length) { + throw new Error('Please enter at least one property for the document to update.'); + } + + properties.customProperty.forEach(property => { + body[property.field] = property.value; + }); + + const docType = this.getNodeParameter('docType', i) as string; + const documentName = this.getNodeParameter('documentName', i) as string; + + responseData = await erpNextApiRequest.call(this, 'PUT', `/api/resource/${docType}/${documentName}`, body); + responseData = responseData.data; + + } + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts b/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts new file mode 100644 index 0000000000..b8da3d99d6 --- /dev/null +++ b/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts @@ -0,0 +1,107 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions +} from 'n8n-workflow'; + +export async function erpNextApiRequest( + this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + query: IDataObject = {}, + uri?: string, + option: IDataObject = {}, +) { + + const credentials = this.getCredentials('erpNextApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + Authorization: `token ${credentials.apiKey}:${credentials.apiSecret}`, + }, + method, + body, + qs: query, + uri: uri || `https://${credentials.subdomain}.erpnext.com${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + + if (!Object.keys(options.body).length) { + delete options.body; + } + + if (!Object.keys(options.qs).length) { + delete options.qs; + } + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error.statusCode === 403) { + throw new Error( + `ERPNext error response [${error.statusCode}]: DocType unavailable.`, + ); + } + + if (error.statusCode === 307) { + throw new Error( + `ERPNext error response [${error.statusCode}]: Please ensure the subdomain is correct.`, + ); + } + + let errorMessages; + if (error?.response?.body?._server_messages) { + const errors = JSON.parse(error.response.body._server_messages); + errorMessages = errors.map((e: string) => JSON.parse(e).message); + throw new Error( + `ARPNext error response [${error.statusCode}]: ${errorMessages.join('|')}`, + ); + } + + throw error; + } +} + +export async function erpNextApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + resource: string, + body: IDataObject, + query: IDataObject = {}, +) { + // tslint:disable-next-line: no-any + const returnData: any[] = []; + + let responseData; + query!.limit_start = 0; + query!.limit_page_length = 1000; + + do { + responseData = await erpNextApiRequest.call(this, method, resource, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + query!.limit_start += query!.limit_page_length - 1; + } while ( + responseData.data.length > 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/ERPNext/erpnext.svg b/packages/nodes-base/nodes/ERPNext/erpnext.svg new file mode 100644 index 0000000000..3bf4e10dbd --- /dev/null +++ b/packages/nodes-base/nodes/ERPNext/erpnext.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/nodes-base/nodes/ERPNext/utils.ts b/packages/nodes-base/nodes/ERPNext/utils.ts new file mode 100644 index 0000000000..3d9852d8eb --- /dev/null +++ b/packages/nodes-base/nodes/ERPNext/utils.ts @@ -0,0 +1,30 @@ +import { + flow, + sortBy, + uniqBy, +} from 'lodash'; + +export type DocumentProperties = { + customProperty: Array<{ field: string; value: string; }>; +}; + +type DocFields = Array<{ name: string, value: string }>; + +const ensureName = (docFields: DocFields) => docFields.filter(o => o.name); +const sortByName = (docFields: DocFields) => sortBy(docFields, ['name']); +const uniqueByName = (docFields: DocFields) => uniqBy(docFields, o => o.name); + +export const processNames = flow(ensureName, sortByName, uniqueByName); + +export const toSQL = (operator: string) => { + const operators: { [key: string]: string } = { + 'is': '=', + 'isNot': '!=', + 'greater': '>', + 'less': '<', + 'equalsGreater': '>=', + 'equalsLess': '<=', + }; + + return operators[operator]; +}; diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 67ba06679a..0c871e01e8 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; import { IDataObject, @@ -948,291 +948,301 @@ export class EditImage implements INodeType { }, }; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const operation = this.getNodeParameter('operation', 0) as string; - const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + item = items[itemIndex]; - const options = this.getNodeParameter('options', {}) as IDataObject; - const cleanupFunctions: Array<() => void> = []; + const operation = this.getNodeParameter('operation', itemIndex) as string; + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; - let gmInstance: gm.State; + const options = this.getNodeParameter('options', itemIndex,{}) as IDataObject; - const requiredOperationParameters: { - [key: string]: string[], - } = { - blur: [ - 'blur', - 'sigma', - ], - border: [ - 'borderColor', - 'borderWidth', - 'borderHeight', - ], - create: [ - 'backgroundColor', - 'height', - 'width', - ], - crop: [ - 'height', - 'positionX', - 'positionY', - 'width', - ], - composite: [ - 'dataPropertyNameComposite', - 'positionX', - 'positionY', - ], - draw: [ - 'color', - 'cornerRadius', - 'endPositionX', - 'endPositionY', - 'primitive', - 'startPositionX', - 'startPositionY', - ], - information: [], - resize: [ - 'height', - 'resizeOption', - 'width', - ], - rotate: [ - 'backgroundColor', - 'rotate', - ], - shear: [ - 'degreesX', - 'degreesY', - ], - text: [ - 'font', - 'fontColor', - 'fontSize', - 'lineLength', - 'positionX', - 'positionY', - 'text', - ], - }; + const cleanupFunctions: Array<() => void> = []; - let operations: IDataObject[] = []; - if (operation === 'multiStep') { - // Operation parameters are already in the correct format - const operationsData = this.getNodeParameter('operations', { operations: [] }) as IDataObject; - operations = operationsData.operations as IDataObject[]; - } else { - // Operation parameters have to first get collected - const operationParameters: IDataObject = {}; - requiredOperationParameters[operation].forEach(parameterName => { - try { - operationParameters[parameterName] = this.getNodeParameter(parameterName); - } catch (e) {} - }); + let gmInstance: gm.State; - operations = [ - { - operation, - ...operationParameters, - }, - ]; - } + const requiredOperationParameters: { + [key: string]: string[], + } = { + blur: [ + 'blur', + 'sigma', + ], + border: [ + 'borderColor', + 'borderWidth', + 'borderHeight', + ], + create: [ + 'backgroundColor', + 'height', + 'width', + ], + crop: [ + 'height', + 'positionX', + 'positionY', + 'width', + ], + composite: [ + 'dataPropertyNameComposite', + 'positionX', + 'positionY', + ], + draw: [ + 'color', + 'cornerRadius', + 'endPositionX', + 'endPositionY', + 'primitive', + 'startPositionX', + 'startPositionY', + ], + information: [], + resize: [ + 'height', + 'resizeOption', + 'width', + ], + rotate: [ + 'backgroundColor', + 'rotate', + ], + shear: [ + 'degreesX', + 'degreesY', + ], + text: [ + 'font', + 'fontColor', + 'fontSize', + 'lineLength', + 'positionX', + 'positionY', + 'text', + ], + }; - if (operations[0].operation !== 'create') { - // "create" generates a new image so does not require any incoming data. - if (item.binary === undefined) { - throw new Error('Item does not contain any binary data.'); - } - - if (item.binary[dataPropertyName as string] === undefined) { - throw new Error(`Item does not contain any binary data with the name "${dataPropertyName}".`); - } - - gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); - gmInstance = gmInstance.background('transparent'); - } - - if (operation === 'information') { - // Just return the information - const imageData = await new Promise((resolve, reject) => { - gmInstance = gmInstance.identify((error, imageData) => { - if (error) { - reject(error); - return; - } - resolve(imageData as unknown as IDataObject); + let operations: IDataObject[] = []; + if (operation === 'multiStep') { + // Operation parameters are already in the correct format + const operationsData = this.getNodeParameter('operations', itemIndex ,{ operations: [] }) as IDataObject; + operations = operationsData.operations as IDataObject[]; + } else { + // Operation parameters have to first get collected + const operationParameters: IDataObject = {}; + requiredOperationParameters[operation].forEach(parameterName => { + try { + operationParameters[parameterName] = this.getNodeParameter(parameterName, itemIndex); + } catch (e) {} }); - }); - item.json = imageData; - return item; - } + operations = [ + { + operation, + ...operationParameters, + }, + ]; + } - for (let i = 0; i < operations.length; i++) { - const operationData = operations[i]; - if (operationData.operation === 'blur') { - gmInstance = gmInstance!.blur(operationData.blur as number, operationData.sigma as number); - } else if (operationData.operation === 'border') { - gmInstance = gmInstance!.borderColor(operationData.borderColor as string).border(operationData.borderWidth as number, operationData.borderHeight as number); - } else if (operationData.operation === 'composite') { - const positionX = operationData.positionX as number; - const positionY = operationData.positionY as number; - - const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; - - if (item.binary![operationData.dataPropertyNameComposite as string] === undefined) { - throw new Error(`Item does not contain any binary data with the name "${operationData.dataPropertyNameComposite}".`); + if (operations[0].operation !== 'create') { + // "create" generates a new image so does not require any incoming data. + if (item.binary === undefined) { + throw new Error('Item does not contain any binary data.'); } - const { fd, path, cleanup } = await file(); - cleanupFunctions.push(cleanup); - await fsWriteFileAsync(fd, Buffer.from(item.binary![operationData.dataPropertyNameComposite as string].data, BINARY_ENCODING)); - - 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); - } else { - gmInstance = gmInstance!.geometry(geometryString).composite(path); + if (item.binary[dataPropertyName as string] === undefined) { + throw new Error(`Item does not contain any binary data with the name "${dataPropertyName}".`); } - if (operations.length !== i + 1) { - // If there are other operations after the current one create a new gm instance - // because else things do get messed up - gmInstance = gm(gmInstance.stream()); - } - } else if (operationData.operation === 'create') { - gmInstance = gm(operationData.width as number, operationData.height as number, operationData.backgroundColor as string); - if (!options.format) { - options.format = 'png'; - } - } else if (operationData.operation === 'crop') { - gmInstance = gmInstance!.crop(operationData.width as number, operationData.height as number, operationData.positionX as number, operationData.positionY as number); - } else if (operationData.operation === 'draw') { - gmInstance = gmInstance!.fill(operationData.color as string); + gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); + gmInstance = gmInstance.background('transparent'); + } - 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 === '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); - } - } else if (operationData.operation === 'resize') { - const resizeOption = operationData.resizeOption as string; - - // By default use "maximumArea" - let option: gm.ResizeOption = '@'; - if (resizeOption === 'ignoreAspectRatio') { - option = '!'; - } else if (resizeOption === 'minimumArea') { - option = '^'; - } else if (resizeOption === 'onlyIfSmaller') { - option = '<'; - } else if (resizeOption === 'onlyIfLarger') { - option = '>'; - } else if (resizeOption === 'percent') { - option = '%'; - } - - gmInstance = gmInstance!.resize(operationData.width as number, operationData.height as number, option); - } else if (operationData.operation === 'rotate') { - gmInstance = gmInstance!.rotate(operationData.backgroundColor as string, operationData.rotate as number); - } else if (operationData.operation === 'shear') { - gmInstance = gmInstance!.shear(operationData.degreesX as number, operationData.degreesY as number); - } else if (operationData.operation === 'text') { - // Split the text in multiple lines - const lines: string[] = []; - let currentLine = ''; - (operationData.text as string).split('\n').forEach((textLine: string) => { - textLine.split(' ').forEach((textPart: string) => { - if ((currentLine.length + textPart.length + 1) > (operationData.lineLength as number)) { - lines.push(currentLine.trim()); - currentLine = `${textPart} `; + if (operation === 'information') { + // Just return the information + const imageData = await new Promise((resolve, reject) => { + gmInstance = gmInstance.identify((error, imageData) => { + if (error) { + reject(error); return; } - currentLine += `${textPart} `; + resolve(imageData as unknown as IDataObject); }); - - lines.push(currentLine.trim()); - currentLine = ''; }); - // Combine the lines to a single string - const renderText = lines.join('\n'); - - const font = options.font || operationData.font; - - if (font && font !== 'default') { - gmInstance = gmInstance!.font(font as string); - } - - gmInstance = gmInstance! - .fill(operationData.fontColor as string) - .fontSize(operationData.fontSize as number) - .drawText(operationData.positionX as number, operationData.positionY as number, renderText); + item.json = imageData; + returnData.push(item); } - } - const newItem: INodeExecutionData = { - json: item.json, - binary: {}, - }; + for (let i = 0; i < operations.length; i++) { + const operationData = operations[i]; + if (operationData.operation === 'blur') { + gmInstance = gmInstance!.blur(operationData.blur as number, operationData.sigma as number); + } else if (operationData.operation === 'border') { + gmInstance = gmInstance!.borderColor(operationData.borderColor as string).border(operationData.borderWidth as number, operationData.borderHeight as number); + } else if (operationData.operation === 'composite') { + const positionX = operationData.positionX as number; + const positionY = operationData.positionY as number; - if (item.binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - Object.assign(newItem.binary, item.binary); - // Make a deep copy of the binary data we change - if (newItem.binary![dataPropertyName as string]) { - newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); - } - } + const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; - if (newItem.binary![dataPropertyName as string] === undefined) { - newItem.binary![dataPropertyName as string] = { - data: '', - mimeType: '', - }; - } - - if (options.quality !== undefined) { - gmInstance = gmInstance!.quality(options.quality as number); - } - - if (options.format !== undefined) { - gmInstance = gmInstance!.setFormat(options.format as string); - newItem.binary![dataPropertyName as string].fileExtension = options.format as string; - newItem.binary![dataPropertyName as string].mimeType = `image/${options.format}`; - const fileName = newItem.binary![dataPropertyName as string].fileName; - if (fileName && fileName.includes('.')) { - newItem.binary![dataPropertyName as string].fileName = fileName.split('.').slice(0, -1).join('.') + '.' + options.format; - } - } - - if (options.fileName !== undefined) { - newItem.binary![dataPropertyName as string].fileName = options.fileName as string; - } - - return new Promise((resolve, reject) => { - gmInstance - .toBuffer((error: Error | null, buffer: Buffer) => { - cleanupFunctions.forEach(async cleanup => await cleanup()); - - if (error) { - return reject(error); + if (item.binary![operationData.dataPropertyNameComposite as string] === undefined) { + throw new Error(`Item does not contain any binary data with the name "${operationData.dataPropertyNameComposite}".`); } - newItem.binary![dataPropertyName as string].data = buffer.toString(BINARY_ENCODING); + const { fd, path, cleanup } = await file(); + cleanupFunctions.push(cleanup); + await fsWriteFileAsync(fd, Buffer.from(item.binary![operationData.dataPropertyNameComposite as string].data, BINARY_ENCODING)); - return resolve(newItem); - }); - }); + 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); + } else { + gmInstance = gmInstance!.geometry(geometryString).composite(path); + } + + if (operations.length !== i + 1) { + // If there are other operations after the current one create a new gm instance + // because else things do get messed up + gmInstance = gm(gmInstance.stream()); + } + } else if (operationData.operation === 'create') { + gmInstance = gm(operationData.width as number, operationData.height as number, operationData.backgroundColor as string); + if (!options.format) { + options.format = 'png'; + } + } else if (operationData.operation === 'crop') { + gmInstance = gmInstance!.crop(operationData.width as number, operationData.height as number, operationData.positionX as number, operationData.positionY as number); + } else if (operationData.operation === 'draw') { + gmInstance = gmInstance!.fill(operationData.color as string); + + 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 === '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); + } + } else if (operationData.operation === 'resize') { + const resizeOption = operationData.resizeOption as string; + + // By default use "maximumArea" + let option: gm.ResizeOption = '@'; + if (resizeOption === 'ignoreAspectRatio') { + option = '!'; + } else if (resizeOption === 'minimumArea') { + option = '^'; + } else if (resizeOption === 'onlyIfSmaller') { + option = '<'; + } else if (resizeOption === 'onlyIfLarger') { + option = '>'; + } else if (resizeOption === 'percent') { + option = '%'; + } + + gmInstance = gmInstance!.resize(operationData.width as number, operationData.height as number, option); + } else if (operationData.operation === 'rotate') { + gmInstance = gmInstance!.rotate(operationData.backgroundColor as string, operationData.rotate as number); + } else if (operationData.operation === 'shear') { + gmInstance = gmInstance!.shear(operationData.degreesX as number, operationData.degreesY as number); + } else if (operationData.operation === 'text') { + // Split the text in multiple lines + const lines: string[] = []; + let currentLine = ''; + (operationData.text as string).split('\n').forEach((textLine: string) => { + textLine.split(' ').forEach((textPart: string) => { + if ((currentLine.length + textPart.length + 1) > (operationData.lineLength as number)) { + lines.push(currentLine.trim()); + currentLine = `${textPart} `; + return; + } + currentLine += `${textPart} `; + }); + + lines.push(currentLine.trim()); + currentLine = ''; + }); + + // Combine the lines to a single string + const renderText = lines.join('\n'); + + const font = options.font || operationData.font; + + if (font && font !== 'default') { + gmInstance = gmInstance!.font(font as string); + } + + gmInstance = gmInstance! + .fill(operationData.fontColor as string) + .fontSize(operationData.fontSize as number) + .drawText(operationData.positionX as number, operationData.positionY as number, renderText); + } + } + + const newItem: INodeExecutionData = { + json: item.json, + binary: {}, + }; + + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, item.binary); + // Make a deep copy of the binary data we change + if (newItem.binary![dataPropertyName as string]) { + newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); + } + } + + if (newItem.binary![dataPropertyName as string] === undefined) { + newItem.binary![dataPropertyName as string] = { + data: '', + mimeType: '', + }; + } + + if (options.quality !== undefined) { + gmInstance = gmInstance!.quality(options.quality as number); + } + + if (options.format !== undefined) { + gmInstance = gmInstance!.setFormat(options.format as string); + newItem.binary![dataPropertyName as string].fileExtension = options.format as string; + newItem.binary![dataPropertyName as string].mimeType = `image/${options.format}`; + const fileName = newItem.binary![dataPropertyName as string].fileName; + if (fileName && fileName.includes('.')) { + newItem.binary![dataPropertyName as string].fileName = fileName.split('.').slice(0, -1).join('.') + '.' + options.format; + } + } + + if (options.fileName !== undefined) { + newItem.binary![dataPropertyName as string].fileName = options.fileName as string; + } + + returnData.push(await (new Promise((resolve, reject) => { + gmInstance + .toBuffer((error: Error | null, buffer: Buffer) => { + cleanupFunctions.forEach(async cleanup => await cleanup()); + + if (error) { + return reject(error); + } + + newItem.binary![dataPropertyName as string].data = buffer.toString(BINARY_ENCODING); + + return resolve(newItem); + }); + }))); + + } + return this.prepareOutputData(returnData); } } diff --git a/packages/nodes-base/nodes/EmailSend.node.ts b/packages/nodes-base/nodes/EmailSend.node.ts index f2ca08ad46..6028d6c547 100644 --- a/packages/nodes-base/nodes/EmailSend.node.ts +++ b/packages/nodes-base/nodes/EmailSend.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions } from 'n8n-core'; import { IDataObject, @@ -124,84 +124,95 @@ export class EmailSend implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - const fromEmail = this.getNodeParameter('fromEmail') as string; - const toEmail = this.getNodeParameter('toEmail') as string; - const ccEmail = this.getNodeParameter('ccEmail') as string; - const bccEmail = this.getNodeParameter('bccEmail') as string; - const subject = this.getNodeParameter('subject') as string; - const text = this.getNodeParameter('text') as string; - const html = this.getNodeParameter('html') as string; - const attachmentPropertyString = this.getNodeParameter('attachments') as string; - const options = this.getNodeParameter('options', {}) as IDataObject; + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const credentials = this.getCredentials('smtp'); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + item = items[itemIndex]; - const connectionOptions: SMTPTransport.Options = { - host: credentials.host as string, - port: credentials.port as number, - secure: credentials.secure as boolean, - }; + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; + const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; + const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; + const bccEmail = this.getNodeParameter('bccEmail', itemIndex) as string; + const subject = this.getNodeParameter('subject', itemIndex) as string; + const text = this.getNodeParameter('text', itemIndex) as string; + const html = this.getNodeParameter('html', itemIndex) as string; + const attachmentPropertyString = this.getNodeParameter('attachments', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; - if(credentials.user || credentials.password) { - // @ts-ignore - connectionOptions.auth = { - user: credentials.user as string, - pass: credentials.password as string, - }; - } + const credentials = this.getCredentials('smtp'); - if (options.allowUnauthorizedCerts === true) { - connectionOptions.tls = { - rejectUnauthorized: false, - }; - } - - const transporter = createTransport(connectionOptions); - - // setup email data with unicode symbols - const mailOptions = { - from: fromEmail, - to: toEmail, - cc: ccEmail, - bcc: bccEmail, - subject, - text, - html, - }; - - if (attachmentPropertyString && item.binary) { - const attachments = []; - const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { - return propertyName.trim(); - }); - - for (const propertyName of attachmentProperties) { - if (!item.binary.hasOwnProperty(propertyName)) { - continue; - } - attachments.push({ - filename: item.binary[propertyName].fileName || 'unknown', - content: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), - }); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); } - if (attachments.length) { + const connectionOptions: SMTPTransport.Options = { + host: credentials.host as string, + port: credentials.port as number, + secure: credentials.secure as boolean, + }; + + if (credentials.user || credentials.password) { // @ts-ignore - mailOptions.attachments = attachments; + connectionOptions.auth = { + user: credentials.user as string, + pass: credentials.password as string, + }; } + + if (options.allowUnauthorizedCerts === true) { + connectionOptions.tls = { + rejectUnauthorized: false, + }; + } + + const transporter = createTransport(connectionOptions); + + // setup email data with unicode symbols + const mailOptions = { + from: fromEmail, + to: toEmail, + cc: ccEmail, + bcc: bccEmail, + subject, + text, + html, + }; + + if (attachmentPropertyString && item.binary) { + const attachments = []; + const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { + return propertyName.trim(); + }); + + for (const propertyName of attachmentProperties) { + if (!item.binary.hasOwnProperty(propertyName)) { + continue; + } + attachments.push({ + filename: item.binary[propertyName].fileName || 'unknown', + content: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), + }); + } + + if (attachments.length) { + // @ts-ignore + mailOptions.attachments = attachments; + } + } + + // Send the email + const info = await transporter.sendMail(mailOptions); + + returnData.push({ json: info }); } - // Send the email - const info = await transporter.sendMail(mailOptions); - - return { json: info }; + return this.prepareOutputData(returnData); } } diff --git a/packages/nodes-base/nodes/Emelia/CampaignDescription.ts b/packages/nodes-base/nodes/Emelia/CampaignDescription.ts new file mode 100644 index 0000000000..c7d6475976 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/CampaignDescription.ts @@ -0,0 +1,326 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const campaignOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Add Contact', + value: 'addContact', + }, + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Pause', + value: 'pause', + }, + { + name: 'Start', + value: 'start', + }, + ], + displayOptions: { + show: { + resource: [ + 'campaign', + ], + }, + }, + }, +] as INodeProperties[]; + +export const campaignFields = [ + // ---------------------------------- + // campaign: addContact + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: [], + required: true, + description: 'The ID of the campaign to add the contact to.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'addContact', + ], + }, + }, + }, + { + displayName: 'Contact Email', + name: 'contactEmail', + type: 'string', + required: true, + default: '', + description: 'The email of the contact to add to the campaign.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'addContact', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'addContact', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields ', + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Name', + name: 'fieldName', + type: 'string', + default: '', + description: 'The name of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact to add.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the contact to add.', + }, + { + displayName: 'Last Contacted', + name: 'lastContacted', + type: 'string', + default: '', + description: 'Last contacted date of the contact to add.', + }, + { + displayName: 'Last Open', + name: 'lastOpen', + type: 'string', + default: '', + description: 'Last opened date of the contact to add.', + }, + { + displayName: 'Last Replied', + name: 'lastReplied', + type: 'string', + default: '', + description: 'Last replied date of the contact to add.', + }, + { + displayName: 'Mails Sent', + name: 'mailsSent', + type: 'number', + default: 0, + description: 'Number of emails sent to the contact to add.', + }, + { + displayName: 'Phone Number', + name: 'phoneNumber', + type: 'string', + default: '', + description: 'Phone number of the contact to add.', + }, + ], + }, + + // ---------------------------------- + // campaign: create + // ---------------------------------- + { + displayName: 'Campaign Name', + name: 'campaignName', + type: 'string', + required: true, + default: '', + description: 'The name of the campaign to create.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------- + // campaign: get + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + required: true, + description: 'The ID of the campaign to retrieve.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // campaign: pause + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + required: true, + description: 'The ID of the campaign to pause.
The campaign must be in RUNNING mode.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'pause', + ], + }, + }, + }, + + // ---------------------------------- + // campaign: start + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + required: true, + description: 'The ID of the campaign to start.
Email provider and contacts must be set.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'start', + ], + }, + }, + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Emelia/ContactListDescription.ts b/packages/nodes-base/nodes/Emelia/ContactListDescription.ts new file mode 100644 index 0000000000..5512f8734d --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/ContactListDescription.ts @@ -0,0 +1,221 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactListOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Add', + value: 'add', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'contactList', + ], + }, + }, + }, +] as INodeProperties[]; + +export const contactListFields = [ + // ---------------------------------- + // contactList: add + // ---------------------------------- + { + displayName: 'Contact List ID', + name: 'contactListId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactLists', + }, + default: [], + required: true, + description: 'The ID of the contact list to add the contact to.', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Contact Email', + name: 'contactEmail', + type: 'string', + required: true, + default: '', + description: 'The email of the contact to add to the contact list.', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'add', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields ', + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Name', + name: 'fieldName', + type: 'string', + default: '', + description: 'The name of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact to add.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the contact to add.', + }, + { + displayName: 'Last Contacted', + name: 'lastContacted', + type: 'dateTime', + default: '', + description: 'Last contacted date of the contact to add.', + }, + { + displayName: 'Last Open', + name: 'lastOpen', + type: 'dateTime', + default: '', + description: 'Last opened date of the contact to add.', + }, + { + displayName: 'Last Replied', + name: 'lastReplied', + type: 'dateTime', + default: '', + description: 'Last replied date of the contact to add.', + }, + { + displayName: 'Mails Sent', + name: 'mailsSent', + type: 'number', + default: 0, + description: 'Number of emails sent to the contact to add.', + }, + { + displayName: 'Phone Number', + name: 'phoneNumber', + type: 'string', + default: '', + description: 'Phone number of the contact to add.', + }, + ], + }, + + // ---------------------------------- + // contactList: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'contactList', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Emelia/Emelia.node.ts b/packages/nodes-base/nodes/Emelia/Emelia.node.ts new file mode 100644 index 0000000000..55b6ebac70 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/Emelia.node.ts @@ -0,0 +1,387 @@ +import { + IExecuteFunctions +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; + +import { + emeliaGraphqlRequest, + loadResource, +} from './GenericFunctions'; + +import { + campaignFields, + campaignOperations, +} from './CampaignDescription'; + +import { + contactListFields, + contactListOperations, +} from './ContactListDescription'; + +import { + isEmpty, +} from 'lodash'; + +export class Emelia implements INodeType { + description: INodeTypeDescription = { + displayName: 'Emelia', + name: 'emelia', + icon: 'file:emelia.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the Emelia API', + defaults: { + name: 'Emelia', + color: '#e18063', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'emeliaApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Contact List', + value: 'contactList', + }, + ], + default: 'campaign', + required: true, + description: 'The resource to operate on.', + }, + ...campaignOperations, + ...campaignFields, + ...contactListOperations, + ...contactListFields, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions) { + return loadResource.call(this, 'campaign'); + }, + + async getContactLists(this: ILoadOptionsFunctions) { + return loadResource.call(this, 'contactList'); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'campaign') { + + // ********************************** + // campaign + // ********************************** + + if (operation === 'addContact') { + + // ---------------------------------- + // campaign: addContact + // ---------------------------------- + + const contact = { + email: this.getNodeParameter('contactEmail', i) as string, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (!isEmpty(additionalFields)) { + Object.assign(contact, additionalFields); + } + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldName}`]: value.value }), {}); + Object.assign(contact, data); + //@ts-ignore + delete contact.customFieldsUi; + } + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + mutation AddContactToCampaignHook($id: ID!, $contact: JSON!) { + addContactToCampaignHook(id: $id, contact: $contact) + }`, + operationName: 'AddContactToCampaignHook', + variables: { + id: this.getNodeParameter('campaignId', i), + contact, + }, + }); + + returnData.push({ contactId: responseData.data.addContactToCampaignHook }); + + } else if (operation === 'create') { + + // ---------------------------------- + // campaign: create + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + operationName: 'createCampaign', + query: ` + mutation createCampaign($name: String!) { + createCampaign(name: $name) { + _id + name + status + createdAt + provider + startAt + estimatedEnd + } + }`, + variables: { + name: this.getNodeParameter('campaignName', i), + }, + }); + + returnData.push(responseData.data.createCampaign); + + } else if (operation === 'get') { + + // ---------------------------------- + // campaign: get + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query campaign($id: ID!){ + campaign(id: $id){ + _id + name + status + createdAt + schedule{ + dailyContact + dailyLimit + minInterval + maxInterval + trackLinks + trackOpens + timeZone + days + start + end + eventToStopMails + } + provider + startAt + recipients{ + total_count + } + estimatedEnd + } + }`, + operationName: 'campaign', + variables: { + id: this.getNodeParameter('campaignId', i), + }, + }); + + returnData.push(responseData.data.campaign); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query all_campaigns { + all_campaigns { + _id + name + status + createdAt + stats { + mailsSent + uniqueOpensPercent + opens + linkClickedPercent + repliedPercent + bouncedPercent + unsubscribePercent + progressPercent + } + } + }`, + operationName: 'all_campaigns', + }); + + let campaigns = responseData.data.all_campaigns; + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + campaigns = campaigns.slice(0, limit); + } + + returnData.push(...campaigns); + + } else if (operation === 'pause') { + + // ---------------------------------- + // campaign: pause + // ---------------------------------- + + await emeliaGraphqlRequest.call(this, { + query: ` + mutation pauseCampaign($id: ID!) { + pauseCampaign(id: $id) + }`, + operationName: 'pauseCampaign', + variables: { + id: this.getNodeParameter('campaignId', i), + }, + }); + + returnData.push({ success: true }); + + } else if (operation === 'start') { + + // ---------------------------------- + // campaign: start + // ---------------------------------- + + await emeliaGraphqlRequest.call(this, { + query: ` + mutation startCampaign($id: ID!) { + startCampaign(id: $id) + }`, + operationName: 'startCampaign', + variables: { + id: this.getNodeParameter('campaignId', i), + }, + }); + + returnData.push({ success: true }); + + } + + } else if (resource === 'contactList') { + + // ********************************** + // ContactList + // ********************************** + + if (operation === 'add') { + + // ---------------------------------- + // contactList: add + // ---------------------------------- + + const contact = { + email: this.getNodeParameter('contactEmail', i) as string, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (!isEmpty(additionalFields)) { + Object.assign(contact, additionalFields); + } + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldName}`]: value.value }), {}); + Object.assign(contact, data); + //@ts-ignore + delete contact.customFieldsUi; + } + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + mutation AddContactsToListHook($id: ID!, $contact: JSON!) { + addContactsToListHook(id: $id, contact: $contact) + }`, + operationName: 'AddContactsToListHook', + variables: { + id: this.getNodeParameter('contactListId', i), + contact, + }, + }); + + returnData.push({ contactId: responseData.data.addContactsToListHook }); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // contactList: getAll + // ---------------------------------- + + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query contact_lists{ + contact_lists{ + _id + name + contactCount + fields + usedInCampaign + } + }`, + operationName: 'contact_lists', + }); + + let contactLists = responseData.data.contact_lists; + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + contactLists = contactLists.slice(0, limit); + } + + returnData.push(...contactLists); + } + + } + + } catch (error) { + + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + + throw error; + + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts b/packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts new file mode 100644 index 0000000000..b2c8aa4c56 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/EmeliaTrigger.node.ts @@ -0,0 +1,180 @@ +import { + IHookFunctions, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookFunctions, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + emeliaApiRequest, + emeliaGraphqlRequest, +} from './GenericFunctions'; + +interface Campaign { + _id: string; + name: string; +} + +export class EmeliaTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Emelia Trigger', + name: 'emeliaTrigger', + icon: 'file:emelia.svg', + group: ['trigger'], + version: 1, + description: 'Handle Emelia campaign activity events via webhooks', + defaults: { + name: 'Emelia Trigger', + color: '#e18063', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'emeliaApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + required: true, + default: '', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + options: [ + { + name: 'Email Bounced', + value: 'bounced', + }, + { + name: 'Email Opened', + value: 'opened', + }, + { + name: 'Email Replied', + value: 'replied', + }, + { + name: 'Email Sent', + value: 'sent', + }, + { + name: 'Link Clicked', + value: 'clicked', + }, + { + name: 'Unsubscribed Contact', + value: 'unsubscribed', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions): Promise { + const responseData = await emeliaGraphqlRequest.call(this, { + query: ` + query GetCampaigns { + campaigns { + _id + name + } + }`, + operationName: 'GetCampaigns', + variables: '{}', + }); + + return responseData.data.campaigns.map( + (campaign: Campaign) => ({ + name: campaign.name, + value: campaign._id, + }), + ); + }, + }, + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const campaignId = this.getNodeParameter('campaignId') as string; + const { webhooks } = await emeliaApiRequest.call(this, 'GET', '/webhook'); + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && webhook.campaignId === campaignId) { + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string[]; + + const campaignId = this.getNodeParameter('campaignId') as string; + const body = { + hookUrl: webhookUrl, + events: events.map(e => e.toUpperCase()), + campaignId, + }; + + const { webhookId } = await emeliaApiRequest.call(this, 'POST', '/webhook/webhook', body); + webhookData.webhookId = webhookId; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const campaignId = this.getNodeParameter('campaignId') as string; + + try { + const body = { + hookUrl: webhookUrl, + campaignId, + }; + await emeliaApiRequest.call(this, 'DELETE', '/webhook/webhook', body); + } catch (error) { + return false; + } + + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Emelia/GenericFunctions.ts b/packages/nodes-base/nodes/Emelia/GenericFunctions.ts new file mode 100644 index 0000000000..e5577b9680 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/GenericFunctions.ts @@ -0,0 +1,104 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IHookFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +/** + * Make an authenticated GraphQL request to Emelia. + */ +export async function emeliaGraphqlRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + body: object = {}, +) { + const response = await emeliaApiRequest.call(this, 'POST', '/graphql', body); + + if (response.errors) { + throw new Error(`Emelia error message: ${response.errors[0].message}`); + } + + return response; +} + +/** + * Make an authenticated REST API request to Emelia, used for trigger node. + */ +export async function emeliaApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, + method: string, + endpoint: string, + body: object = {}, + qs: object = {}, +) { + const { apiKey } = this.getCredentials('emeliaApi') as { apiKey: string }; + + const options = { + headers: { + Authorization: apiKey, + }, + method, + body, + qs, + uri: `https://graphql.emelia.io${endpoint}`, + json: true, + }; + + try { + + return await this.helpers.request!.call(this, options); + + } catch (error) { + + if (error?.response?.body?.error) { + const { error: errorMessage } = error.response.body; + throw new Error( + `Emelia error response [${error.statusCode}]: ${errorMessage}`, + ); + } + + throw error; + } +} + +/** + * Load resources so that the user can select them easily. + */ +export async function loadResource( + this: ILoadOptionsFunctions, + resource: 'campaign' | 'contactList', +): Promise { + const mapping: { [key in 'campaign' | 'contactList']: { query: string, key: string } } = { + campaign: { + query: ` + query GetCampaigns { + campaigns { + _id + name + } + }`, + key: 'campaigns', + }, + contactList: { + query: ` + query GetContactLists { + contact_lists { + _id + name + } + }`, + key: 'contact_lists', + }, + }; + + const responseData = await emeliaGraphqlRequest.call(this, { query: mapping[resource].query }); + + return responseData.data[mapping[resource].key].map((campaign: { name: string, _id: string }) => ({ + name: campaign.name, + value: campaign._id, + })); + +} diff --git a/packages/nodes-base/nodes/Emelia/emelia.svg b/packages/nodes-base/nodes/Emelia/emelia.svg new file mode 100644 index 0000000000..2344b9b6a3 --- /dev/null +++ b/packages/nodes-base/nodes/Emelia/emelia.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index d8a9aa2d3c..1b5582de7f 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -84,6 +84,18 @@ export class FacebookGraphApi implements INodeType { name: 'Default', value: '', }, + { + name: 'v10.0', + value: 'v10.0', + }, + { + name: 'v9.0', + value: 'v9.0', + }, + { + name: 'v8.0', + value: 'v8.0', + }, { name: 'v7.0', value: 'v7.0', diff --git a/packages/nodes-base/nodes/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem.node.ts index cd0eae6df9..e7032e460e 100644 --- a/packages/nodes-base/nodes/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem.node.ts @@ -1,4 +1,4 @@ -import { IExecuteSingleFunctions } from 'n8n-core'; +import { IExecuteFunctions } from 'n8n-core'; import { IBinaryKeyData, IDataObject, @@ -40,74 +40,84 @@ export class FunctionItem implements INodeType { ], }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - let item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - // Copy the items as they may get changed in the functions - item = JSON.parse(JSON.stringify(item)); + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - // Define the global objects for the custom function - const sandbox = { - getBinaryData: (): IBinaryKeyData | undefined => { - return item.binary; - }, - getNodeParameter: this.getNodeParameter, - getWorkflowStaticData: this.getWorkflowStaticData, - helpers: this.helpers, - item: item.json, - setBinaryData: (data: IBinaryKeyData) => { - item.binary = data; - }, - }; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { - // Make it possible to access data via $node, $parameter, ... - const dataProxy = this.getWorkflowDataProxy(); - Object.assign(sandbox, dataProxy); + item = items[itemIndex]; - const options = { - console: 'inherit', - sandbox, - require: { - external: false as boolean | { modules: string[] }, - builtin: [] as string[], - }, - }; + // Copy the items as they may get changed in the functions + item = JSON.parse(JSON.stringify(item)); - if (process.env.NODE_FUNCTION_ALLOW_BUILTIN) { - options.require.builtin = process.env.NODE_FUNCTION_ALLOW_BUILTIN.split(','); + // Define the global objects for the custom function + const sandbox = { + getBinaryData: (): IBinaryKeyData | undefined => { + return item.binary; + }, + getNodeParameter: this.getNodeParameter, + getWorkflowStaticData: this.getWorkflowStaticData, + helpers: this.helpers, + item: item.json, + setBinaryData: (data: IBinaryKeyData) => { + item.binary = data; + }, + }; + + // Make it possible to access data via $node, $parameter, ... + const dataProxy = this.getWorkflowDataProxy(itemIndex); + Object.assign(sandbox, dataProxy); + + const options = { + console: 'inherit', + sandbox, + require: { + external: false as boolean | { modules: string[] }, + builtin: [] as string[], + }, + }; + + if (process.env.NODE_FUNCTION_ALLOW_BUILTIN) { + options.require.builtin = process.env.NODE_FUNCTION_ALLOW_BUILTIN.split(','); + } + + if (process.env.NODE_FUNCTION_ALLOW_EXTERNAL) { + options.require.external = { modules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL.split(',') }; + } + + const vm = new NodeVM(options); + + // Get the code to execute + const functionCode = this.getNodeParameter('functionCode', itemIndex) as string; + + + let jsonData: IDataObject; + try { + // Execute the function code + jsonData = await vm.run(`module.exports = async function() {${functionCode}}()`, __dirname); + } catch (e) { + return Promise.reject(e); + } + + // Do very basic validation of the data + if (jsonData === undefined) { + throw new Error('No data got returned. Always an object has to be returned!'); + } + + const returnItem: INodeExecutionData = { + json: jsonData, + }; + + if (item.binary) { + returnItem.binary = item.binary; + } + + returnData.push(returnItem); } - - if (process.env.NODE_FUNCTION_ALLOW_EXTERNAL) { - options.require.external = { modules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL.split(',') }; - } - - const vm = new NodeVM(options); - - // Get the code to execute - const functionCode = this.getNodeParameter('functionCode') as string; - - - let jsonData: IDataObject; - try { - // Execute the function code - jsonData = await vm.run(`module.exports = async function() {${functionCode}}()`, __dirname); - } catch (e) { - return Promise.reject(e); - } - - // Do very basic validation of the data - if (jsonData === undefined) { - throw new Error('No data got returned. Always an object has to be returned!'); - } - - const returnItem: INodeExecutionData = { - json: jsonData, - }; - - if (item.binary) { - returnItem.binary = item.binary; - } - - return returnItem; + return this.prepareOutputData(returnData); } } diff --git a/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.json b/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.json new file mode 100644 index 0000000000..52c74af1e5 --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.getResponseTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication", + "Marketing & Content" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/getResponse" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.getResponseTrigger/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index e3f38a40f3..39aae86ee6 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -130,27 +130,27 @@ export class Github implements INodeType { { name: 'Create', value: 'create', - description: 'Create a new issue', + description: 'Create a new issue.', }, { name: 'Create Comment', value: 'createComment', - description: 'Create a new comment on an issue', + description: 'Create a new comment on an issue.', }, { name: 'Edit', value: 'edit', - description: 'Edit an issue', + description: 'Edit an issue.', }, { name: 'Get', value: 'get', - description: 'Get the data of a single issues', + description: 'Get the data of a single issue.', }, { name: 'Lock', value: 'lock', - description: 'Lock an issue', + description: 'Lock an issue.', }, ], default: 'create', @@ -172,22 +172,22 @@ export class Github implements INodeType { { name: 'Create', value: 'create', - description: 'Create a new file in repository', + description: 'Create a new file in repository.', }, { name: 'Delete', value: 'delete', - description: 'Delete a file in repository', + description: 'Delete a file in repository.', }, { name: 'Edit', value: 'edit', - description: 'Edit a file in repository', + description: 'Edit a file in repository.', }, { name: 'Get', value: 'get', - description: 'Get the data of a single issue', + description: 'Get the data of a single issue.', }, ], default: 'create', @@ -209,22 +209,22 @@ export class Github implements INodeType { { name: 'Get', value: 'get', - description: 'Get the data of a single repository', + description: 'Get the data of a single repository.', }, { name: 'Get License', value: 'getLicense', - description: 'Returns the contents of the repository\'s license file, if one is detected', + description: 'Returns the contents of the repository\'s license file, if one is detected.', }, { name: 'Get Issues', value: 'getIssues', - description: 'Returns issues of a repository', + description: 'Returns issues of a repository.', }, { name: 'Get Profile', value: 'getProfile', - description: 'Get the community profile of a repository with metrics, health score, description, license, ...', + description: 'Get the community profile of a repository with metrics, health score, description, license, etc.', }, { name: 'List Popular Paths', @@ -234,7 +234,7 @@ export class Github implements INodeType { { name: 'List Referrers', value: 'listReferrers', - description: 'Get the top 10 referrering domains over the last 14 days', + description: 'Get the top 10 referrering domains over the last 14 days.', }, ], default: 'getIssues', @@ -256,7 +256,7 @@ export class Github implements INodeType { { name: 'Get Repositories', value: 'getRepositories', - description: 'Returns the repositories of a user', + description: 'Returns the repositories of a user.', }, { name: 'Invite', @@ -283,7 +283,7 @@ export class Github implements INodeType { { name: 'Create', value: 'create', - description: 'Creates a new release', + description: 'Creates a new release.', }, ], default: 'create', @@ -305,22 +305,22 @@ export class Github implements INodeType { { name: 'Create', value: 'create', - description: 'Creates a new review', + description: 'Creates a new review.', }, { name: 'Get', value: 'get', - description: 'Get a review for a pull request', + description: 'Get a review for a pull request.', }, { name: 'Get All', value: 'getAll', - description: 'Get all reviews for a pull request', + description: 'Get all reviews for a pull request.', }, { name: 'Update', value: 'update', - description: 'Update a review', + description: 'Update a review.', }, ], default: 'create', @@ -815,12 +815,12 @@ export class Github implements INodeType { { name: 'Closed', value: 'closed', - description: 'Set the state to "closed"', + description: 'Set the state to "closed".', }, { name: 'Open', value: 'open', - description: 'Set the state to "open"', + description: 'Set the state to "open".', }, ], default: 'open', @@ -860,7 +860,7 @@ export class Github implements INodeType { name: 'assignee', type: 'string', default: '', - description: 'User to assign issue too.', + description: 'User to assign issue to.', }, ], }, @@ -1046,6 +1046,47 @@ export class Github implements INodeType { // ---------------------------------- // repository:getIssues // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'repository', + ], + operation: [ + 'getIssues', + ], + }, + }, + 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: [ + 'repository', + ], + operation: [ + 'getIssues', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, { displayName: 'Filters', name: 'getRepositoryIssuesFilters', @@ -1108,17 +1149,17 @@ export class Github implements INodeType { { name: 'All', value: 'all', - description: 'Returns issues with any state', + description: 'Returns issues with any state.', }, { name: 'Closed', value: 'closed', - description: 'Return issues with "closed" state', + description: 'Return issues with "closed" state.', }, { name: 'Open', value: 'open', - description: 'Return issues with "open" state', + description: 'Return issues with "open" state.', }, ], default: 'open', @@ -1132,17 +1173,17 @@ export class Github implements INodeType { { name: 'Created', value: 'created', - description: 'Sort by created date', + description: 'Sort by created date.', }, { name: 'Updated', value: 'updated', - description: 'Sort by updated date', + description: 'Sort by updated date.', }, { name: 'Comments', value: 'comments', - description: 'Sort by comments', + description: 'Sort by comments.', }, ], default: 'created', @@ -1156,12 +1197,12 @@ export class Github implements INodeType { { name: 'Ascending', value: 'asc', - description: 'Sort in ascending order', + description: 'Sort in ascending order.', }, { name: 'Descending', value: 'desc', - description: 'Sort in descending order', + description: 'Sort in descending order.', }, ], default: 'desc', @@ -1214,7 +1255,7 @@ export class Github implements INodeType { ], }, }, - description: 'ID of the review', + description: 'ID of the review.', }, // ---------------------------------- @@ -1318,17 +1359,17 @@ export class Github implements INodeType { { name: 'Approve', value: 'approve', - description: 'Approve the pull request', + description: 'Approve the pull request.', }, { name: 'Request Change', value: 'requestChanges', - description: 'Request code changes', + description: 'Request code changes.', }, { name: 'Comment', value: 'comment', - description: 'Add a comment without approval or change requests', + description: 'Add a comment without approval or change requests.', }, { name: 'Pending', @@ -1386,7 +1427,7 @@ export class Github implements INodeType { name: 'commitId', type: 'string', default: '', - description: 'The SHA of the commit that needs a review, if different from the latest', + description: 'The SHA of the commit that needs a review, if different from the latest.', }, ], }, @@ -1414,6 +1455,50 @@ export class Github implements INodeType { description: 'The body of the review', }, // ---------------------------------- + // user:getRepositories + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getRepositories', + ], + }, + }, + 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: [ + 'user', + ], + operation: [ + 'getRepositories', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + // ---------------------------------- // user:invite // ---------------------------------- { @@ -1736,6 +1821,12 @@ export class Github implements INodeType { qs = this.getNodeParameter('getRepositoryIssuesFilters', i) as IDataObject; endpoint = `/repos/${owner}/${repository}/issues`; + + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll === false) { + qs.per_page = this.getNodeParameter('limit', 0) as number; + } } } else if (resource === 'review') { if (operation === 'get') { @@ -1791,7 +1882,7 @@ export class Github implements INodeType { const reviewId = this.getNodeParameter('reviewId', i) as string; body.body = this.getNodeParameter('body', i) as string; - + endpoint = `/repos/${owner}/${repository}/pulls/${pullRequestNumber}/reviews/${reviewId}`; } } else if (resource === 'user') { @@ -1804,13 +1895,19 @@ export class Github implements INodeType { endpoint = `/users/${owner}/repos`; - } else if (operation === 'invite') { + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll === false) { + qs.per_page = this.getNodeParameter('limit', 0) as number; + } + + } else if (operation === 'invite') { // ---------------------------------- // invite // ---------------------------------- requestMethod = 'POST'; - const org = this.getNodeParameter('organization', i) as string; + const org = this.getNodeParameter('organization', i) as string; endpoint = `/orgs/${org}/invitations`; body.email = this.getNodeParameter('email', i) as string; diff --git a/packages/nodes-base/nodes/Github/github.svg b/packages/nodes-base/nodes/Github/github.svg index 933c133ff3..811c46c1c9 100644 --- a/packages/nodes-base/nodes/Github/github.svg +++ b/packages/nodes-base/nodes/Github/github.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts b/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts index cd429b48f0..6056af734b 100644 --- a/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts +++ b/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts @@ -17,7 +17,7 @@ export class Gitlab implements INodeType { description: INodeTypeDescription = { displayName: 'GitLab', name: 'gitlab', - icon: 'file:gitlab.png', + icon: 'file:gitlab.svg', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts index 9839e68532..66412981ec 100644 --- a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -18,7 +18,7 @@ export class GitlabTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'GitLab Trigger', name: 'gitlabTrigger', - icon: 'file:gitlab.png', + icon: 'file:gitlab.svg', group: ['trigger'], version: 1, subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', diff --git a/packages/nodes-base/nodes/Gitlab/gitlab.png b/packages/nodes-base/nodes/Gitlab/gitlab.png deleted file mode 100644 index 9aae9cc6f4..0000000000 Binary files a/packages/nodes-base/nodes/Gitlab/gitlab.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Gitlab/gitlab.svg b/packages/nodes-base/nodes/Gitlab/gitlab.svg new file mode 100644 index 0000000000..9f0f809743 --- /dev/null +++ b/packages/nodes-base/nodes/Gitlab/gitlab.svg @@ -0,0 +1,15 @@ + + + gitlab_node_icon + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/GoToWebinar/GenericFunctions.ts b/packages/nodes-base/nodes/GoToWebinar/GenericFunctions.ts new file mode 100644 index 0000000000..6f35da1f8d --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/GenericFunctions.ts @@ -0,0 +1,286 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +import * as moment from 'moment'; + +import * as losslessJSON from 'lossless-json'; + +/** + * Make an authenticated API request to GoToWebinar. + */ +export async function goToWebinarApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject | IDataObject[], + option: IDataObject = {}, +) { + + const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + method, + uri: `https://api.getgo.com/G2W/rest/v2/${endpoint}`, + qs, + body: JSON.stringify(body), + json: false, + }; + + if (resource === 'session' && operation === 'getAll') { + options.headers!['Accept'] = 'application/vnd.citrix.g2wapi-v1.1+json'; + } + + if (['GET', 'DELETE'].includes(method)) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + try { + const response = await this.helpers.requestOAuth2!.call(this, 'goToWebinarOAuth2Api', options, { tokenExpiredStatusCode: 403 }); + + if (response === '') { + return {}; + } + + // https://stackoverflow.com/questions/62190724/getting-gotowebinar-registrant + return losslessJSON.parse(response, convertLosslessNumber); + } catch (error) { + + if (error?.response?.body) { + let errorMessage; + const body = JSON.parse(error.response.body); + if (Array.isArray(body.validationErrorCodes)) { + errorMessage = (body.validationErrorCodes as IDataObject[]).map((e) => e.description).join('|'); + } else { + errorMessage = body.description; + } + throw new Error(`Go To Webinar error response [${error.statusCode}]: ${errorMessage}`); + } + + throw error; + } +} + +/** + * Make an authenticated API request to GoToWebinar and return all results. + */ +export async function goToWebinarApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject, + resource: string, +) { + + const resourceToResponseKey: { [key: string]: string } = { + session: 'sessionInfoResources', + webinar: 'webinars', + }; + + const key = resourceToResponseKey[resource]; + + let returnData: IDataObject[] = []; + let responseData; + + do { + responseData = await goToWebinarApiRequest.call(this, method, endpoint, qs, body); + + if (responseData.page && parseInt(responseData.page.totalElements, 10) === 0) { + return []; + } else if (responseData._embedded && responseData._embedded[key]) { + returnData.push(...responseData._embedded[key]); + } else { + returnData.push(...responseData); + } + + if (qs.limit && returnData.length >= qs.limit) { + returnData = returnData.splice(0, qs.limit as number); + return returnData; + } + + } while ( + responseData.totalElements && parseInt(responseData.totalElements, 10) > returnData.length + ); + + return returnData; +} + +export async function handleGetAll( + this: IExecuteFunctions, + endpoint: string, + qs: IDataObject, + body: IDataObject, + resource: string) { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', 0) as number; + } + + return await goToWebinarApiRequestAllItems.call(this, 'GET', endpoint, qs, body, resource); +} + +export async function loadWebinars(this: ILoadOptionsFunctions) { + const { oauthTokenData } = this.getCredentials('goToWebinarOAuth2Api') as { + oauthTokenData: { account_key: string } + }; + + const endpoint = `accounts/${oauthTokenData.account_key}/webinars`; + + const qs = { + fromTime: moment().subtract(1, 'years').format(), + toTime: moment().add(1, 'years').format(), + }; + + const resourceItems = await goToWebinarApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, 'webinar'); + + const returnData: INodePropertyOptions[] = []; + + resourceItems.forEach((item) => { + returnData.push({ + name: item.subject as string, + value: item.webinarKey as string, + }); + }); + + return returnData; +} + +export async function loadWebinarSessions(this: ILoadOptionsFunctions) { + const { oauthTokenData } = this.getCredentials('goToWebinarOAuth2Api') as { + oauthTokenData: { organizer_key: string } + }; + + const webinarKey = this.getCurrentNodeParameter('webinarKey') as string; + + const endpoint = `organizers/${oauthTokenData.organizer_key}/webinars/${webinarKey}/sessions`; + + const resourceItems = await goToWebinarApiRequestAllItems.call(this, 'GET', endpoint, {}, {}, 'session'); + + const returnData: INodePropertyOptions[] = []; + + resourceItems.forEach((item) => { + returnData.push({ + name: `Date: ${moment(item.startTime as string).format('MM-DD-YYYY')} | From: ${moment(item.startTime as string).format('LT')} - To: ${moment(item.endTime as string).format('LT')}`, + value: item.sessionKey as string, + }); + }); + + return returnData; +} + +export async function loadRegistranSimpleQuestions(this: ILoadOptionsFunctions) { + const { oauthTokenData } = this.getCredentials('goToWebinarOAuth2Api') as { + oauthTokenData: { organizer_key: string } + }; + + const webinarkey = this.getNodeParameter('webinarKey') as string; + + const endpoint = `organizers/${oauthTokenData.organizer_key}/webinars/${webinarkey}/registrants/fields`; + + const { questions } = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + const returnData: INodePropertyOptions[] = []; + + questions.forEach((item: IDataObject) => { + if (item.type === 'shortAnswer') { + returnData.push({ + name: item.question as string, + value: item.questionKey as string, + }); + } + }); + + return returnData; +} + +export async function loadAnswers(this: ILoadOptionsFunctions) { + const { oauthTokenData } = this.getCredentials('goToWebinarOAuth2Api') as { + oauthTokenData: { organizer_key: string } + }; + + const webinarKey = this.getCurrentNodeParameter('webinarKey') as string; + + const questionKey = this.getCurrentNodeParameter('questionKey') as string; + + const endpoint = `organizers/${oauthTokenData.organizer_key}/webinars/${webinarKey}/registrants/fields`; + + const { questions } = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + const returnData: INodePropertyOptions[] = []; + + questions.forEach((item: IDataObject) => { + if (item.type === 'multiChoice' && item.questionKey === questionKey) { + for (const answer of item.answers as IDataObject[]) { + returnData.push({ + name: answer.answer as string, + value: answer.answerKey as string, + }); + } + } + }); + + return returnData; +} + +export async function loadRegistranMultiChoiceQuestions(this: ILoadOptionsFunctions) { + const { oauthTokenData } = this.getCredentials('goToWebinarOAuth2Api') as { + oauthTokenData: { organizer_key: string } + }; + + const webinarkey = this.getNodeParameter('webinarKey') as string; + + const endpoint = `organizers/${oauthTokenData.organizer_key}/webinars/${webinarkey}/registrants/fields`; + + const { questions } = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + const returnData: INodePropertyOptions[] = []; + + questions.forEach((item: IDataObject) => { + if (item.type === 'multipleChoice') { + returnData.push({ + name: item.question as string, + value: item.questionKey as string, + }); + } + }); + + return returnData; +} + +// tslint:disable-next-line: no-any +function convertLosslessNumber(key: any, value: any) { + if (value && value.isLosslessNumber) { + return value.toString(); + } + else { + return value; + } +} diff --git a/packages/nodes-base/nodes/GoToWebinar/GoToWebinar.node.ts b/packages/nodes-base/nodes/GoToWebinar/GoToWebinar.node.ts new file mode 100644 index 0000000000..3fda2cd72c --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/GoToWebinar.node.ts @@ -0,0 +1,690 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + attendeeFields, + attendeeOperations, + coorganizerFields, + coorganizerOperations, + panelistFields, + panelistOperations, + registrantFields, + registrantOperations, + sessionFields, + sessionOperations, + webinarFields, + webinarOperations, +} from './descriptions'; + +import { + goToWebinarApiRequest, + goToWebinarApiRequestAllItems, + handleGetAll, + loadAnswers, + loadRegistranMultiChoiceQuestions, + loadRegistranSimpleQuestions, + loadWebinars, + loadWebinarSessions, +} from './GenericFunctions'; + +import { + isEmpty, + omit, +} from 'lodash'; + +import * as moment from 'moment-timezone'; + +export class GoToWebinar implements INodeType { + description: INodeTypeDescription = { + displayName: 'GoToWebinar', + name: 'goToWebinar', + icon: 'file:gotowebinar.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the GoToWebinar API', + defaults: { + name: 'GoToWebinar', + color: '#0097e1', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'goToWebinarOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Attendee', + value: 'attendee', + }, + { + name: 'Co-Organizer', + value: 'coorganizer', + }, + { + name: 'Panelist', + value: 'panelist', + }, + { + name: 'Registrant', + value: 'registrant', + }, + { + name: 'Session', + value: 'session', + }, + { + name: 'Webinar', + value: 'webinar', + }, + ], + default: 'attendee', + description: 'Resource to consume', + }, + ...attendeeOperations, + ...attendeeFields, + ...coorganizerOperations, + ...coorganizerFields, + ...panelistOperations, + ...panelistFields, + ...registrantOperations, + ...registrantFields, + ...sessionOperations, + ...sessionFields, + ...webinarOperations, + ...webinarFields, + ], + }; + + methods = { + loadOptions: { + async getWebinars(this: ILoadOptionsFunctions) { + return await loadWebinars.call(this); + }, + async getAnswers(this: ILoadOptionsFunctions) { + return await loadAnswers.call(this); + }, + async getWebinarSessions(this: ILoadOptionsFunctions) { + return await loadWebinarSessions.call(this); + }, + // Get all the timezones to display them to user so that he can + // select them easily + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId, + }); + } + return returnData; + }, + async getRegistranSimpleQuestions(this: ILoadOptionsFunctions): Promise { + return await loadRegistranSimpleQuestions.call(this); + }, + async getRegistranMultiChoiceQuestions(this: ILoadOptionsFunctions): Promise { + return await loadRegistranMultiChoiceQuestions.call(this); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + const { oauthTokenData } = this.getCredentials('goToWebinarOAuth2Api') as { + oauthTokenData: { account_key: string, organizer_key: string } + }; + + const accountKey = oauthTokenData.account_key; + const organizerKey = oauthTokenData.organizer_key; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'attendee') { + + // ********************************************************************* + // attendee + // ********************************************************************* + + // https://developer.goto.com/GoToWebinarV2/#tag/Attendees + + if (operation === 'get') { + + // ---------------------------------- + // attendee: get + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const sessionKey = this.getNodeParameter('sessionKey', i) as string; + const registrantKey = this.getNodeParameter('registrantKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/sessions/${sessionKey}/attendees/${registrantKey}`; + responseData = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // attendee: getAll + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const sessionKey = this.getNodeParameter('sessionKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/sessions/${sessionKey}/attendees`; + responseData = await handleGetAll.call(this, endpoint, {}, {}, resource); + + } else if (operation === 'getDetails') { + + // ---------------------------------- + // attendee: getDetails + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const sessionKey = this.getNodeParameter('sessionKey', i) as string; + const registrantKey = this.getNodeParameter('registrantKey', i) as string; + const details = this.getNodeParameter('details', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/sessions/${sessionKey}/attendees/${registrantKey}/${details}`; + responseData = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + } + + } else if (resource === 'coorganizer') { + + // ********************************************************************* + // coorganizer + // ********************************************************************* + + // https://developer.goto.com/GoToWebinarV2/#tag/Co-organizers + + if (operation === 'create') { + + // ---------------------------------- + // coorganizer: create + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const body = { + external: this.getNodeParameter('isExternal', i) as boolean, + } as IDataObject; + + if (body.external === false) { + body.organizerKey = this.getNodeParameter('organizerKey', i) as string; + } + + if (body.external === true) { + body.givenName = this.getNodeParameter('givenName', i) as string; + body.email = this.getNodeParameter('email', i) as string; + } + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/coorganizers`; + responseData = await goToWebinarApiRequest.call(this, 'POST', endpoint, {}, [body]); + + } else if (operation === 'delete') { + + // ---------------------------------- + // coorganizer: delete + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const coorganizerKey = this.getNodeParameter('coorganizerKey', i) as string; + + const qs = { + external: this.getNodeParameter('isExternal', i) as boolean, + }; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/coorganizers/${coorganizerKey}`; + responseData = await goToWebinarApiRequest.call(this, 'DELETE', endpoint, qs, {}); + responseData = { success: true }; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // coorganizer: getAll + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/coorganizers`; + responseData = await handleGetAll.call(this, endpoint, {}, {}, resource); + + } else if (operation === 'reinvite') { + + // ---------------------------------- + // coorganizer: reinvite + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const coorganizerKey = this.getNodeParameter('coorganizerKey', i) as string; + + const qs = { + external: this.getNodeParameter('isExternal', i) as boolean, + }; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/coorganizers/${coorganizerKey}/resendInvitation`; + + responseData = await goToWebinarApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = { success: true }; + + } + + } else if (resource === 'panelist') { + + // ********************************************************************* + // panelist + // ********************************************************************* + + // https://developer.goto.com/GoToWebinarV2/#tag/Panelists + + if (operation === 'create') { + + // ---------------------------------- + // panelist: create + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const body = [ + { + name: this.getNodeParameter('name', i) as string, + email: this.getNodeParameter('email', i) as string, + }, + ] as IDataObject[]; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/panelists`; + responseData = await goToWebinarApiRequest.call(this, 'POST', endpoint, {}, body); + + } else if (operation === 'delete') { + + // ---------------------------------- + // panelist: delete + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const panelistKey = this.getNodeParameter('panelistKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/panelists/${panelistKey}`; + responseData = await goToWebinarApiRequest.call(this, 'DELETE', endpoint, {}, {}); + responseData = { success: true }; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // panelist: getAll + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/panelists`; + responseData = await handleGetAll.call(this, endpoint, {}, {}, resource); + + } else if (operation === 'reinvite') { + + // ---------------------------------- + // panelist: reinvite + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const panelistKey = this.getNodeParameter('panelistKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/panelists/${panelistKey}/resendInvitation`; + responseData = await goToWebinarApiRequest.call(this, 'POST', endpoint, {}, {}); + responseData = { success: true }; + + } + + } else if (resource === 'registrant') { + + // ********************************************************************* + // registrant + // ********************************************************************* + + // https://developer.goto.com/GoToWebinarV2/#tag/Registrants + + if (operation === 'create') { + + // ---------------------------------- + // registrant: create + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const qs = {} as IDataObject; + const body = { + firstName: this.getNodeParameter('firstName', i) as string, + lastName: this.getNodeParameter('lastName', i) as string, + email: this.getNodeParameter('email', i) as string, + responses: [], + } as IDataObject; + + let additionalFields = this.getNodeParameter('additionalFields', i) as Partial<{ + resendConfirmation: boolean, + fullAddress: { + details: { [key: string]: string } + } + simpleResponses: [ + { [key: string]: string } + ], + multiChoiceResponses: [ + { [key: string]: string } + ], + }>; + + if (additionalFields.resendConfirmation) { + qs.resendConfirmation = additionalFields.resendConfirmation; + additionalFields = omit(additionalFields, ['resendConfirmation']); + } + + if (additionalFields.fullAddress) { + Object.assign(body, additionalFields.fullAddress.details); + additionalFields = omit(additionalFields, ['fullAddress']); + } + + if (additionalFields.simpleResponses) { + //@ts-ignore + body.responses.push(...additionalFields.simpleResponses.details); + additionalFields = omit(additionalFields, ['simpleResponses']); + } + + if (additionalFields.multiChoiceResponses) { + //@ts-ignore + body.responses.push(...additionalFields.multiChoiceResponses.details); + additionalFields = omit(additionalFields, ['multiChoiceResponses']); + } + + Object.assign(body, additionalFields); + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/registrants`; + responseData = await goToWebinarApiRequest.call(this, 'POST', endpoint, qs, body); + + } else if (operation === 'delete') { + + // ---------------------------------- + // registrant: delete + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const registrantKey = this.getNodeParameter('registrantKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/registrants/${registrantKey}`; + responseData = await goToWebinarApiRequest.call(this, 'DELETE', endpoint, {}, {}); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------- + // registrant: get + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const registrantKey = this.getNodeParameter('registrantKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/registrants/${registrantKey}`; + responseData = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // registrant: getAll + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/registrants`; + responseData = await handleGetAll.call(this, endpoint, {}, {}, resource); + } + + } else if (resource === 'session') { + + // ********************************************************************* + // session + // ********************************************************************* + + // https://developer.goto.com/GoToWebinarV2/#tag/Sessions + + if (operation === 'get') { + + // ---------------------------------- + // session: get + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const sessionKey = this.getNodeParameter('sessionKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/sessions/${sessionKey}`; + responseData = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // session: getAll + // ---------------------------------- + + const qs = {} as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', 0) as number; + } + + const { + webinarKey, + times, + } = this.getNodeParameter('additionalFields', i) as { + filterByWebinar: boolean, + webinarKey: string, + times: { + timesProperties: { [key: string]: string } + } + }; + + if (times) { + qs.fromTime = moment(times.timesProperties.fromTime).format(); + qs.toTime = moment(times.timesProperties.toTime).format(); + } else { + qs.fromTime = moment().subtract(1, 'years').format(); + qs.toTime = moment().format(); + } + + if (webinarKey !== undefined) { + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/sessions`; + responseData = await goToWebinarApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + + } else { + + const endpoint = `organizers/${organizerKey}/sessions`; + responseData = await goToWebinarApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + } + + } else if (operation === 'getDetails') { + + // ---------------------------------- + // session: getDetails + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + const sessionKey = this.getNodeParameter('sessionKey', i) as string; + const details = this.getNodeParameter('details', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}/sessions/${sessionKey}/${details}`; + responseData = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + } + + } else if (resource === 'webinar') { + + // ********************************************************************* + // webinar + // ********************************************************************* + + // https://developer.goto.com/GoToWebinarV2/#tag/Webinars + + if (operation === 'create') { + + // ---------------------------------- + // webinar: create + // ---------------------------------- + + const timesProperties = this.getNodeParameter('times.timesProperties', i, []) as IDataObject; + + const body = { + subject: this.getNodeParameter('subject', i) as string, + times: timesProperties, + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(body, additionalFields); + + const endpoint = `organizers/${organizerKey}/webinars`; + responseData = await goToWebinarApiRequest.call(this, 'POST', endpoint, {}, body); + + } else if (operation === 'delete') { + + // ---------------------------------- + // webinar: delete + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const { sendCancellationEmails } = this.getNodeParameter('additionalFields', i) as IDataObject; + + const qs = {} as IDataObject; + + if (sendCancellationEmails) { + qs.sendCancellationEmails = sendCancellationEmails; + } + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}`; + await goToWebinarApiRequest.call(this, 'DELETE', endpoint, qs, {}); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------- + // webinar: get + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}`; + responseData = await goToWebinarApiRequest.call(this, 'GET', endpoint, {}, {}); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // webinar: getAll + // ---------------------------------- + + const qs = {} as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', 0) as number; + } + + const { times } = this.getNodeParameter('additionalFields', i) as { + times: { + timesProperties: { [key: string]: string } + } + }; + + if (times) { + qs.fromTime = moment(times.timesProperties.fromTime).format(); + qs.toTime = moment(times.timesProperties.toTime).format(); + } else { + qs.fromTime = moment().subtract(1, 'years').format(); + qs.toTime = moment().format(); + } + + const endpoint = `accounts/${accountKey}/webinars`; + responseData = await goToWebinarApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // webinar: update + // ---------------------------------- + + const webinarKey = this.getNodeParameter('webinarKey', i) as string; + + const qs = { + notifyParticipants: this.getNodeParameter('notifyParticipants', i) as boolean, + } as IDataObject; + + let body = {}; + + let updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (updateFields.times) { + const { times } = updateFields as { + times: { timesProperties: Array<{ startTime: string, endTime: string }> } + }; + + body = { + times: times.timesProperties, + } as IDataObject; + + updateFields = omit(updateFields, ['times']); + } + + Object.assign(body, updateFields); + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + const endpoint = `organizers/${organizerKey}/webinars/${webinarKey}`; + await goToWebinarApiRequest.call(this, 'PUT', endpoint, qs, body); + responseData = { success: true }; + } + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/AttendeeDescription.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/AttendeeDescription.ts new file mode 100644 index 0000000000..709e9a2c00 --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/AttendeeDescription.ts @@ -0,0 +1,203 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const attendeeOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Get Details', + value: 'getDetails', + }, + ], + displayOptions: { + show: { + resource: [ + 'attendee', + ], + }, + }, + }, +] as INodeProperties[]; + +export const attendeeFields = [ + // ---------------------------------- + // attendee: shared fields + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: '', + description: 'Key of the webinar that the attendee attended.', + displayOptions: { + show: { + resource: [ + 'attendee', + ], + }, + }, + }, + { + displayName: 'Session Key', + name: 'sessionKey', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getWebinarSessions', + loadOptionsDependsOn: [ + 'webinarKey', + ], + }, + default: '', + description: 'Key of the session that the attendee attended.', + displayOptions: { + show: { + resource: [ + 'attendee', + ], + }, + }, + }, + + // ---------------------------------- + // attendee: get + // ---------------------------------- + { + displayName: 'Registrant Key', + name: 'registrantKey', + type: 'string', + required: true, + default: '', + description: 'Registrant key of the attendee at the webinar session.', + displayOptions: { + show: { + resource: [ + 'attendee', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // attendee: getDetails + // ---------------------------------- + { + displayName: 'Registrant Key', + name: 'registrantKey', + type: 'string', + required: true, + default: '', + description: 'Registrant key of the attendee at the webinar session.', + displayOptions: { + show: { + resource: [ + 'attendee', + ], + operation: [ + 'getDetails', + ], + }, + }, + }, + { + displayName: 'Details', + name: 'details', + type: 'options', + required: true, + default: '', + description: 'The details to retrieve for the attendee.', + options: [ + { + name: 'Polls', + value: 'polls', + description: 'Poll answers from the attendee in a webinar session.', + }, + { + name: 'Questions', + value: 'questions', + description: 'Questions asked by the attendee in a webinar session.', + }, + { + name: 'Survey Answers', + value: 'surveyAnswers', + description: 'Survey answers from the attendee in a webinar session.', + }, + ], + displayOptions: { + show: { + resource: [ + 'attendee', + ], + operation: [ + 'getDetails', + ], + }, + }, + }, + + // ---------------------------------- + // attendee: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'attendee', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'attendee', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/CoorganizerDescription.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/CoorganizerDescription.ts new file mode 100644 index 0000000000..fddebb22bd --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/CoorganizerDescription.ts @@ -0,0 +1,327 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const coorganizerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Reinvite', + value: 'reinvite', + }, + ], + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + }, + }, + }, +] as INodeProperties[]; + +export const coorganizerFields = [ + // ---------------------------------- + // coorganizer: create + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar that the co-organizer is hosting.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Is External', + name: 'isExternal', + type: 'boolean', + required: true, + default: false, + description: 'Whether the co-organizer has no GoToWebinar account.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Organizer Key', + name: 'organizerKey', + type: 'string', + description: 'The co-organizer\'s organizer key for the webinar.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'create', + ], + isExternal: [ + false, + ], + }, + }, + }, + { + displayName: 'Given Name', + name: 'givenName', + type: 'string', + default: '', + description: 'The co-organizer\'s given name.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'create', + ], + isExternal: [ + true, + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The co-organizer\'s email address.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'create', + ], + isExternal: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // coorganizer: delete + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar to delete.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Co-Organizer Key', + name: 'coorganizerKey', + type: 'string', + default: '', + description: 'Key of the co-organizer to delete.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Is External', + name: 'isExternal', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'delete', + ], + }, + }, + description: `By default only internal co-organizers (with a GoToWebinar account) can be deleted.
+ If you want to use this call for external co-organizers you have to set this parameter to 'true'.`, + }, + + // ---------------------------------- + // coorganizer: getAll + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar to retrieve all co-organizers from.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // coorganizer: reinvite + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'string', + required: true, + default: '', + description: `By default only internal co-organizers (with a GoToWebinar account) can be deleted.
+ If you want to use this call for external co-organizers you have to set this parameter to 'true'.`, + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'reinvite', + ], + }, + }, + }, + { + displayName: 'Co-Organizer Key', + name: 'coorganizerKey', + type: 'string', + default: '', + description: 'Key of the co-organizer to reinvite.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'reinvite', + ], + }, + }, + }, + { + displayName: 'Is External', + name: 'isExternal', + type: 'boolean', + required: true, + default: false, + description: 'Whether the co-organizer has no GoToWebinar account.', + displayOptions: { + show: { + resource: [ + 'coorganizer', + ], + operation: [ + 'reinvite', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/PanelistDescription.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/PanelistDescription.ts new file mode 100644 index 0000000000..d31ef9948f --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/PanelistDescription.ts @@ -0,0 +1,253 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const panelistOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Reinvite', + value: 'reinvite', + }, + ], + displayOptions: { + show: { + resource: [ + 'panelist', + ], + }, + }, + }, +] as INodeProperties[]; + +export const panelistFields = [ + // ---------------------------------- + // panelist: create + // ---------------------------------- + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + description: 'Name of the panelist to create.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'Email address of the panelist to create.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar that the panelist will present at.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------- + // panelist: getAll + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar to retrieve all panelists from.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // panelist: delete + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar to delete the panelist from.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Panelist Key', + name: 'panelistKey', + type: 'string', + required: true, + default: '', + description: 'Key of the panelist to delete.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // panelist: reinvite + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar to reinvite the panelist to.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'reinvite', + ], + }, + }, + }, + { + displayName: 'Panelist Key', + name: 'panelistKey', + type: 'string', + required: true, + default: '', + description: 'Key of the panelist to reinvite.', + displayOptions: { + show: { + resource: [ + 'panelist', + ], + operation: [ + 'reinvite', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/RegistrantDescription.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/RegistrantDescription.ts new file mode 100644 index 0000000000..078f557bb5 --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/RegistrantDescription.ts @@ -0,0 +1,475 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const registrantOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'registrant', + ], + }, + }, + }, +] as INodeProperties[]; + +export const registrantFields = [ + // ---------------------------------- + // registrant: create + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar of the registrant to create.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the registrant to create.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the registrant to create.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address of the registrant to create.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Full Address', + name: 'fullAddress', + placeholder: 'Add Address Fields', + type: 'fixedCollection', + description: 'Full address of the registrant to create.', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'The type of industry the registrant\'s organization belongs to.', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + }, + { + displayName: 'MultiChoice Responses', + name: 'multiChoiceResponses', + placeholder: 'Add MultiChoice Response', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Set the answers to all questions.', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Question Key', + name: 'questionKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getRegistranMultiChoiceQuestions', + loadOptionsDependsOn: [ + 'webinarKey', + ], + }, + default: '', + }, + { + displayName: 'Answer Key', + name: 'AnswerKey', + type: 'string', + default: '', + description: 'Answer ID of the question.', + }, + ], + }, + ], + }, + { + displayName: 'Number of Employees', + name: 'numberOfEmployees', + type: 'string', + default: '', + description: 'The size in employees of the registrant\'s organization.', + }, + { + displayName: 'Organization', + name: 'organization', + type: 'string', + default: '', + }, + { + displayName: 'Telephone', + name: 'phone', + type: 'string', + default: '', + }, + { + displayName: 'Purchasing Role', + name: 'purchasingRole', + type: 'string', + default: '', + description: 'Registrant\'s role in purchasing the product.', + }, + { + displayName: 'Purchasing Time Frame', + name: 'purchasingTimeFrame', + type: 'string', + default: '', + description: 'Time frame within which the product will be purchased.', + }, + { + displayName: 'Questions and Comments', + name: 'questionsAndComments', + type: 'string', + default: '', + description: 'Questions or comments made by the registrant during registration.', + }, + { + displayName: 'Resend Confirmation', + name: 'resendConfirmation', + type: 'boolean', + default: false, + }, + { + displayName: 'Simple Responses', + name: 'simpleResponses', + placeholder: 'Add Simple Response', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Set the answers to all questions.', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Question Key', + name: 'questionKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getRegistranSimpleQuestions', + loadOptionsDependsOn: [ + 'webinarKey', + ], + }, + default: '', + }, + { + displayName: 'Response Text', + name: 'responseText', + type: 'string', + default: '', + description: 'Text of the response to the question.', + }, + ], + }, + ], + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + description: 'The source that led to the registration.', + }, + ], + }, + + // ---------------------------------- + // registrant: getAll + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'The key of the webinar to retrieve registrants from.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // registrant: delete + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar of the registrant to delete.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Registrant Key', + name: 'registrantKey', + type: 'string', + required: true, + default: '', + description: 'Key of the registrant to delete.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // registrant: get + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar of the registrant to retrieve.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Registrant Key', + name: 'registrantKey', + type: 'string', + required: true, + default: '', + description: 'Key of the registrant to retrieve.', + displayOptions: { + show: { + resource: [ + 'registrant', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/SessionDescription.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/SessionDescription.ts new file mode 100644 index 0000000000..b9545237e5 --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/SessionDescription.ts @@ -0,0 +1,226 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const sessionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Get Details', + value: 'getDetails', + }, + ], + displayOptions: { + show: { + resource: [ + 'session', + ], + }, + }, + }, +] as INodeProperties[]; + +export const sessionFields = [ + // ---------------------------------- + // session: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'session', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'session', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'session', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Time Range', + name: 'times', + type: 'fixedCollection', + placeholder: 'Add Time Range', + required: true, + default: {}, + options: [ + { + displayName: 'Times Properties', + name: 'timesProperties', + values: [ + { + displayName: 'Start Time', + name: 'fromTime', + type: 'dateTime', + description: 'Start of the datetime range for the session.', + default: '', + }, + { + displayName: 'End Time', + name: 'toTime', + type: 'dateTime', + description: 'End of the datetime range for the session.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + default: {}, + description: 'Webinar by which to filter the sessions to retrieve.', + }, + ], + }, + + // ---------------------------------- + // session: shared fields + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWebinars', + }, + required: true, + default: [], + description: 'Key of the webinar to which the session belongs.', + displayOptions: { + show: { + resource: [ + 'session', + ], + operation: [ + 'get', + 'getDetails', + ], + }, + }, + }, + { + displayName: 'Session Key', + name: 'sessionKey', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'session', + ], + operation: [ + 'get', + 'getDetails', + ], + }, + }, + }, + + // ---------------------------------- + // session: getDetails + // ---------------------------------- + { + displayName: 'Details', + name: 'details', + type: 'options', + default: 'performance', + options: [ + { + name: 'Performance', + value: 'performance', + description: 'Performance details for a webinar session.', + }, + { + name: 'Polls', + value: 'polls', + description: 'Questions and answers for polls from a webinar session.', + }, + { + name: 'Questions', + value: 'questions', + description: 'Questions and answers for a past webinar session.', + }, + { + name: 'Surveys', + value: 'surveys', + description: 'Surveys for a past webinar session.', + }, + ], + displayOptions: { + show: { + resource: [ + 'session', + ], + operation: [ + 'getDetails', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/WebinarDescription.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/WebinarDescription.ts new file mode 100644 index 0000000000..c612d2e012 --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/WebinarDescription.ts @@ -0,0 +1,543 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const webinarOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + // { + // name: 'Delete', + // value: 'delete', + // }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'webinar', + ], + }, + }, + }, +] as INodeProperties[]; + +export const webinarFields = [ + // ---------------------------------- + // webinar: create + // ---------------------------------- + { + displayName: 'Subject', + name: 'subject', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Time Range', + name: 'times', + type: 'fixedCollection', + required: true, + placeholder: 'Add Time Range', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Times Properties', + name: 'timesProperties', + values: [ + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + required: true, + default: '', + }, + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + required: true, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Experience Type', + name: 'experienceType', + type: 'options', + default: 'CLASSIC', + options: [ + { + name: 'Classic', + value: 'CLASSIC', + }, + { + name: 'Broadcast', + value: 'BROADCAST', + }, + { + name: 'Simulive', + value: 'SIMULIVE', + }, + ], + }, + { + displayName: 'Is On-Demand', + name: 'isOnDemand', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Password Protected', + name: 'isPasswordProtected', + type: 'boolean', + default: false, + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + required: true, + default: '', + placeholder: '2020-12-11T09:00:00Z', + typeOptions: { + alwaysOpenEditWindow: true, + loadOptionsMethod: 'getTimezones', + }, + }, + { + displayName: 'Webinar Type', + name: 'type', + type: 'options', + default: 'single_session', + options: [ + { + name: 'Single Session', + value: 'single_session', + description: 'Webinar with one single meeting.', + }, + { + name: 'Series', + value: 'series', + description: 'Webinar with multiple meetings times where attendees choose only one to attend.', + }, + { + name: 'Sequence', + value: 'sequence', + description: 'Webinar with multiple meeting times where attendees are expected to be the same for all sessions.', + }, + ], + }, + ], + }, + + // ---------------------------------- + // webinar: delete + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'string', + required: true, + default: '', + description: 'Key of the webinar to delete.', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'delete', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Send Cancellation E-mails', + name: 'sendCancellationEmails', + type: 'boolean', + default: false, + }, + ], + }, + + // ---------------------------------- + // webinar: get + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'string', + required: true, + default: '', + description: 'Key of the webinar to retrieve.', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // webinar: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 10, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Time Range', + name: 'times', + type: 'fixedCollection', + placeholder: 'Add Time Range', + required: true, + default: {}, + options: [ + { + displayName: 'Times Properties', + name: 'timesProperties', + values: [ + { + displayName: 'Start Time', + name: 'fromTime', + type: 'dateTime', + description: 'Start of the datetime range for the webinar.', + default: '', + }, + { + displayName: 'End Time', + name: 'toTime', + type: 'dateTime', + description: 'End of the datetime range for the webinar.', + default: '', + }, + ], + }, + ], + }, + ], + }, + + // ---------------------------------- + // webinar: update + // ---------------------------------- + { + displayName: 'Webinar Key', + name: 'webinarKey', + type: 'string', + required: true, + default: '', + description: 'Key of the webinar to update.', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Notify Participants', + name: 'notifyParticipants', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'webinar', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Experience Type', + name: 'experienceType', + type: 'options', + default: 'CLASSIC', + options: [ + { + name: 'Classic', + value: 'CLASSIC', + }, + { + name: 'Broadcast', + value: 'BROADCAST', + }, + { + name: 'Simulive', + value: 'SIMULIVE', + }, + ], + }, + { + displayName: 'Is On-Demand', + name: 'isOnDemand', + type: 'boolean', + default: false, + description: 'Whether the webinar may be watched anytime.', + }, + { + displayName: 'Is Password Protected', + name: 'isPasswordProtected', + type: 'boolean', + default: false, + description: 'Whether the webinar requires a password for attendees to join.', + }, + { + displayName: 'Times', + name: 'times', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Times Properties', + name: 'timesProperties', + values: [ + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + required: true, + default: '', + }, + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + required: true, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'Name or topic of the webinar.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + required: true, + default: '', + placeholder: '2020-12-11T09:00:00Z', + description: 'Timezone where the webinar is to take place.', + typeOptions: { + alwaysOpenEditWindow: true, + loadOptionsMethod: 'getTimezones', + }, + }, + { + displayName: 'Webinar Type', + name: 'type', + type: 'options', + default: 'single_session', + options: [ + { + name: 'Single Session', + value: 'single_session', + description: 'Webinar with one single meeting.', + }, + { + name: 'Series', + value: 'series', + description: 'Webinar with multiple meetings times where attendees choose only one to attend.', + }, + { + name: 'Sequence', + value: 'sequence', + description: 'Webinar with multiple meeting times where attendees are expected to be the same for all sessions.', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GoToWebinar/descriptions/index.ts b/packages/nodes-base/nodes/GoToWebinar/descriptions/index.ts new file mode 100644 index 0000000000..be6f31829f --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/descriptions/index.ts @@ -0,0 +1,6 @@ +export * from './AttendeeDescription'; +export * from './CoorganizerDescription'; +export * from './PanelistDescription'; +export * from './RegistrantDescription'; +export * from './SessionDescription'; +export * from './WebinarDescription'; diff --git a/packages/nodes-base/nodes/GoToWebinar/gotowebinar.svg b/packages/nodes-base/nodes/GoToWebinar/gotowebinar.svg new file mode 100644 index 0000000000..9241f4fa93 --- /dev/null +++ b/packages/nodes-base/nodes/GoToWebinar/gotowebinar.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts index 035e8f0787..b1ed3e50cd 100644 --- a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts +++ b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts @@ -48,7 +48,7 @@ export const eventOperations = [ export const eventFields = [ /* -------------------------------------------------------------------------- */ - /* event:ALL */ + /* event:getAll */ /* -------------------------------------------------------------------------- */ { displayName: 'Calendar ID', @@ -300,6 +300,13 @@ export const eventFields = [ }, default: 1, }, + { + displayName: 'RRULE', + name: 'rrule', + type: 'string', + default: '', + description: 'Recurrence rule. When set, the parameters Repeat Frecuency, Repeat How Many Times and Repeat Until are ignored.', + }, { displayName: 'Send Updates', name: 'sendUpdates', @@ -920,6 +927,13 @@ export const eventFields = [ }, default: 1, }, + { + displayName: 'RRULE', + name: 'rrule', + type: 'string', + default: '', + description: 'Recurrence rule. When set, the parameters Repeat Frecuency, Repeat How Many Times and Repeat Until are ignored.', + }, { displayName: 'Start', name: 'start', diff --git a/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts b/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts index 2800333a1c..53471ca18f 100644 --- a/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts +++ b/packages/nodes-base/nodes/Google/Calendar/EventInterface.ts @@ -1,6 +1,6 @@ import { IDataObject, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export interface IReminder { useDefault?: boolean; diff --git a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts index a2eed8ea96..edc841710c 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -47,7 +47,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF } } -export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts index 803446de98..e66f718ecb 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts @@ -38,7 +38,7 @@ export class GoogleCalendar implements INodeType { description: INodeTypeDescription = { displayName: 'Google Calendar', name: 'googleCalendar', - icon: 'file:googleCalendar.png', + icon: 'file:googleCalendar.svg', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -321,33 +321,37 @@ export class GoogleCalendar implements INodeType { //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html body.recurrence = []; - if ( - additionalFields.repeatHowManyTimes && - additionalFields.repeatUntil - ) { - throw new Error( - `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, - ); - } - if (additionalFields.repeatFrecuency) { - body.recurrence?.push( - `FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`, - ); - } - if (additionalFields.repeatHowManyTimes) { - body.recurrence?.push( - `COUNT=${additionalFields.repeatHowManyTimes};`, - ); - } - if (additionalFields.repeatUntil) { - body.recurrence?.push( - `UNTIL=${moment(additionalFields.repeatUntil as string) - .utc() - .format('YYYYMMDDTHHmmss')}Z`, - ); - } - if (body.recurrence.length !== 0) { - body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + if (additionalFields.rrule) { + body.recurrence = [`RRULE:${additionalFields.rrule}`]; + } else { + if ( + additionalFields.repeatHowManyTimes && + additionalFields.repeatUntil + ) { + throw new Error( + `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, + ); + } + if (additionalFields.repeatFrecuency) { + body.recurrence?.push( + `FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`, + ); + } + if (additionalFields.repeatHowManyTimes) { + body.recurrence?.push( + `COUNT=${additionalFields.repeatHowManyTimes};`, + ); + } + if (additionalFields.repeatUntil) { + body.recurrence?.push( + `UNTIL=${moment(additionalFields.repeatUntil as string) + .utc() + .format('YYYYMMDDTHHmmss')}Z`, + ); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } } if (additionalFields.conferenceDataUi) { @@ -565,30 +569,34 @@ export class GoogleCalendar implements INodeType { //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html body.recurrence = []; - if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) { - throw new Error( - `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, - ); - } - if (updateFields.repeatFrecuency) { - body.recurrence?.push( - `FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`, - ); - } - if (updateFields.repeatHowManyTimes) { - body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); - } - if (updateFields.repeatUntil) { - body.recurrence?.push( - `UNTIL=${moment(updateFields.repeatUntil as string) - .utc() - .format('YYYYMMDDTHHmmss')}Z`, - ); - } - if (body.recurrence.length !== 0) { - body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + if (updateFields.rrule) { + body.recurrence = [`RRULE:${updateFields.rrule}`]; } else { - delete body.recurrence; + if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) { + throw new Error( + `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, + ); + } + if (updateFields.repeatFrecuency) { + body.recurrence?.push( + `FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`, + ); + } + if (updateFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); + } + if (updateFields.repeatUntil) { + body.recurrence?.push( + `UNTIL=${moment(updateFields.repeatUntil as string) + .utc() + .format('YYYYMMDDTHHmmss')}Z`, + ); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } else { + delete body.recurrence; + } } responseData = await googleApiRequest.call( this, diff --git a/packages/nodes-base/nodes/Google/Calendar/googleCalendar.png b/packages/nodes-base/nodes/Google/Calendar/googleCalendar.png deleted file mode 100644 index 31e365fdbd..0000000000 Binary files a/packages/nodes-base/nodes/Google/Calendar/googleCalendar.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Google/Calendar/googleCalendar.svg b/packages/nodes-base/nodes/Google/Calendar/googleCalendar.svg new file mode 100644 index 0000000000..634c13601d --- /dev/null +++ b/packages/nodes-base/nodes/Google/Calendar/googleCalendar.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts index 5e455898ef..bf6d4a5c71 100644 --- a/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts @@ -45,8 +45,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF const { access_token } = await getAccessToken.call(this, credentials as IDataObject); options.headers!.Authorization = `Bearer ${access_token}`; - //@ts-ignore - return await this.helpers.request(options); + return await this.helpers.request!(options); } else { //@ts-ignore return await this.helpers.requestOAuth2.call(this, 'googleDriveOAuth2Api', options); @@ -140,6 +139,5 @@ function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoa json: true, }; - //@ts-ignore - return this.helpers.request(options); + return this.helpers.request!(options); } diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts index fb17dbaa80..3e776a1fe7 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts @@ -10,13 +10,13 @@ import { INodeTypeDescription, } from 'n8n-workflow'; -import uuid = require('uuid'); - import { googleApiRequest, googleApiRequestAllItems, } from './GenericFunctions'; +import uuid = require('uuid'); + export class GoogleDrive implements INodeType { description: INodeTypeDescription = { displayName: 'Google Drive', @@ -136,6 +136,11 @@ export class GoogleDrive implements INodeType { value: 'share', description: 'Share a file', }, + { + name: 'Update', + value: 'update', + description: 'Update a file', + }, { name: 'Upload', value: 'upload', @@ -205,7 +210,6 @@ export class GoogleDrive implements INodeType { description: 'The ID of the file to copy.', }, - // ---------------------------------- // file/folder:delete // ---------------------------------- @@ -522,7 +526,7 @@ export class GoogleDrive implements INodeType { name: 'permissionsUi', placeholder: 'Add Permission', type: 'fixedCollection', - default: '', + default: {}, typeOptions: { multipleValues: false, }, @@ -710,7 +714,174 @@ export class GoogleDrive implements INodeType { placeholder: '', description: 'Name of the binary property which contains
the data for the file to be uploaded.', }, - + // ---------------------------------- + // file:update + // ---------------------------------- + { + displayName: 'ID', + name: 'fileId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'file', + ], + }, + }, + description: 'The ID of the file to update.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'file', + ], + }, + }, + options: [ + { + displayName: 'Keep Revision Forever', + name: 'keepRevisionForever', + type: 'boolean', + default: false, + description: `Whether to set the 'keepForever' field in the new head revision.
+ his is only applicable to files with binary content in Google Drive.
+ Only 200 revisions for the file can be kept forever. If the limit is reached, try deleting pinned revisions.`, + }, + { + displayName: 'OCR Language', + name: 'ocrLanguage', + type: 'string', + default: '', + description: `A language hint for OCR processing during image import (ISO 639-1 code).`, + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: `The ID of the parent to set.`, + }, + { + displayName: 'Use Content As Indexable Text', + name: 'useContentAsIndexableText', + type: 'boolean', + default: false, + description: `Whether to use the uploaded content as indexable text.`, + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'file', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + description: 'All fields.', + }, + { + name: 'explicitlyTrashed', + value: 'explicitlyTrashed', + }, + { + name: 'exportLinks', + value: 'exportLinks', + }, + { + name: 'iconLink', + value: 'iconLink', + }, + { + name: 'hasThumbnail', + value: 'hasThumbnail', + }, + { + name: 'id', + value: 'id', + }, + { + name: 'kind', + value: 'kind', + }, + { + name: 'name', + value: 'name', + }, + { + name: 'mimeType', + value: 'mimeType', + }, + { + name: 'permissions', + value: 'permissions', + }, + { + name: 'shared', + value: 'shared', + }, + { + name: 'spaces', + value: 'spaces', + }, + { + name: 'starred', + value: 'starred', + }, + { + name: 'thumbnailLink', + value: 'thumbnailLink', + }, + { + name: 'trashed', + value: 'trashed', + }, + { + name: 'version', + value: 'version', + }, + { + name: 'webViewLink', + value: 'webViewLink', + }, + ], + required: true, + default: [], + description: 'The fields to return.', + }, + ], + }, // ---------------------------------- // file:upload // ---------------------------------- @@ -733,6 +904,24 @@ export class GoogleDrive implements INodeType { placeholder: 'invoice_1.pdf', description: 'The name the file should be saved as.', }, + // ---------------------------------- + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + description: 'By default the response only contain the ID of the file.
If this option gets activated it will resolve the data automatically.', + }, { displayName: 'Parents', name: 'parents', @@ -787,9 +976,16 @@ export class GoogleDrive implements INodeType { placeholder: 'Add Option', default: {}, displayOptions: { - hide: { - resource: [ - 'drive', + show: { + '/operation': [ + 'copy', + 'list', + 'share', + 'create', + ], + '/resource': [ + 'file', + 'folder', ], }, }, @@ -838,48 +1034,8 @@ export class GoogleDrive implements INodeType { displayOptions: { show: { '/operation': [ - 'share', - ], - '/resource': [ - 'file', - 'folder', - ], - }, - }, - options: [ - { - name: '*', - value: '*', - description: 'All fields.', - }, - { - name: 'Email Address', - value: 'emailAddress', - }, - { - name: 'Display Name', - value: 'displayName', - }, - { - name: 'Deleted', - value: 'deleted', - }, - ], - default: [], - description: 'The fields to return.', - }, - { - displayName: 'Fields', - name: 'fields', - type: 'multiOptions', - displayOptions: { - hide: { - '/operation': [ - 'share', - ], - '/resource': [ - 'file', - 'folder', + 'list', + 'copy', ], }, }, @@ -1840,7 +1996,11 @@ export class GoogleDrive implements INodeType { } } - const response = await googleApiRequest.call(this, 'POST', `/drive/v3/files/${fileId}/copy`, body); + const qs = { + supportsAllDrives: true, + }; + + const response = await googleApiRequest.call(this, 'POST', `/drive/v3/files/${fileId}/copy`, body, qs); returnData.push(response as IDataObject); @@ -1976,7 +2136,7 @@ export class GoogleDrive implements INodeType { // ---------------------------------- // upload // ---------------------------------- - const options = this.getNodeParameter('options', i) as IDataObject; + const resolveData = this.getNodeParameter('resolveData', 0) as boolean; let mimeType = 'text/plain'; let body; @@ -2042,7 +2202,33 @@ export class GoogleDrive implements INodeType { response = await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${JSON.parse(response).id}`, body, qs); + if (resolveData === true) { + response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${response.id}`, {}, { fields: '*' }); + } + returnData.push(response as IDataObject); + } else if (operation === 'update') { + // ---------------------------------- + // file:update + // ---------------------------------- + + const id = this.getNodeParameter('fileId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i, {}) as IDataObject; + + const qs: IDataObject = { + supportsAllDrives: true, + }; + + Object.assign(qs, options); + + qs.fields = queryFields; + + if (updateFields.parentId && updateFields.parentId !== '') { + qs.addParents = updateFields.parentId; + } + + const responseData = await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${id}`, {}, qs); + returnData.push(responseData as IDataObject); } } else if (resource === 'folder') { @@ -2061,6 +2247,7 @@ export class GoogleDrive implements INodeType { const qs = { fields: queryFields, + supportsAllDrives: true, }; const response = await googleApiRequest.call(this, 'POST', '/drive/v3/files', body, qs); @@ -2102,10 +2289,6 @@ export class GoogleDrive implements INodeType { Object.assign(qs, options); - if (qs.fields) { - qs.fields = (qs.fields as string[]).join(','); - } - const response = await googleApiRequest.call(this, 'POST', `/drive/v3/files/${fileId}/permissions`, body, qs); returnData.push(response as IDataObject); diff --git a/packages/nodes-base/nodes/Google/Drive/googleDrive.svg b/packages/nodes-base/nodes/Google/Drive/googleDrive.svg index 8d09ca1271..8ca43d996d 100644 --- a/packages/nodes-base/nodes/Google/Drive/googleDrive.svg +++ b/packages/nodes-base/nodes/Google/Drive/googleDrive.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts index 3dc2eabe92..29490df7a0 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts @@ -41,6 +41,9 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF body, qs, uri: uri || `https://www.googleapis.com${endpoint}`, + qsStringifyOptions:{ + arrayFormat: 'repeat', + }, json: true, }; @@ -216,7 +219,12 @@ function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoa //https://developers.google.com/identity/protocols/oauth2/service-account#httprest const scopes = [ - 'https://www.googleapis.com/auth/books', + 'https://www.googleapis.com/auth/gmail.labels', + 'https://www.googleapis.com/auth/gmail.addons.current.action.compose', + 'https://www.googleapis.com/auth/gmail.addons.current.message.action', + 'https://mail.google.com/', + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/gmail.compose', ]; const now = moment().unix(); diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts index 5601c259c5..158c0de9b0 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts @@ -66,14 +66,14 @@ export class Gmail implements INodeType { description: INodeTypeDescription = { displayName: 'Gmail', name: 'gmail', - icon: 'file:gmail.png', + icon: 'file:gmail.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume the Gmail API', defaults: { name: 'Gmail', - color: '#d93025', + color: '#4285F4', }, inputs: ['main'], outputs: ['main'], diff --git a/packages/nodes-base/nodes/Google/Gmail/gmail.png b/packages/nodes-base/nodes/Google/Gmail/gmail.png deleted file mode 100644 index 1721048070..0000000000 Binary files a/packages/nodes-base/nodes/Google/Gmail/gmail.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Google/Gmail/gmail.svg b/packages/nodes-base/nodes/Google/Gmail/gmail.svg new file mode 100644 index 0000000000..7f6de6b2e9 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/gmail.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts index 21831fc9b7..80526a9ce1 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GenericFunctions.ts @@ -127,3 +127,19 @@ function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoa //@ts-ignore return this.helpers.request(options); } + +// Hex to RGB +export function hexToRgb(hex: string) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, (m, r, g, b) => { + return r + r + g + g + b + b; + }); + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + red: parseInt(result[1], 16), + green: parseInt(result[2], 16), + blue: parseInt(result[3], 16), + } : null; +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index 6d941c8ee4..58782e1e15 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -23,13 +23,14 @@ import { import { googleApiRequest, + hexToRgb, } from './GenericFunctions'; export class GoogleSheets implements INodeType { description: INodeTypeDescription = { displayName: 'Google Sheets ', name: 'googleSheets', - icon: 'file:googlesheets.png', + icon: 'file:googleSheets.svg', group: ['input', 'output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -121,6 +122,11 @@ export class GoogleSheets implements INodeType { value: 'clear', description: 'Clear data from a sheet', }, + { + name: 'Create', + value: 'create', + description: 'Create a new sheet', + }, { name: 'Delete', value: 'delete', @@ -136,6 +142,11 @@ export class GoogleSheets implements INodeType { value: 'read', description: 'Read data from a sheet', }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a sheet', + }, { name: 'Update', value: 'update', @@ -150,7 +161,7 @@ export class GoogleSheets implements INodeType { // All // ---------------------------------- { - displayName: 'Sheet ID', + displayName: 'Spreadsheet ID', name: 'sheetId', type: 'string', displayOptions: { @@ -162,7 +173,7 @@ export class GoogleSheets implements INodeType { }, default: '', required: true, - description: 'The ID of the Google Sheet.
Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/', + description: 'The ID of the Google Spreadsheet.
Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/', }, { displayName: 'Range', @@ -176,7 +187,9 @@ export class GoogleSheets implements INodeType { }, hide: { operation: [ + 'create', 'delete', + 'remove', ], }, }, @@ -185,7 +198,6 @@ export class GoogleSheets implements INodeType { description: 'The table range to read from or to append data to. See the Google documentation for the details.
If it contains multiple sheets it can also be
added like this: "MySheet!A:F"', }, - // ---------------------------------- // Delete // ---------------------------------- @@ -392,8 +404,10 @@ export class GoogleSheets implements INodeType { hide: { operation: [ 'append', + 'create', 'clear', 'delete', + 'remove', ], rawData: [ true, @@ -422,7 +436,9 @@ export class GoogleSheets implements INodeType { hide: { operation: [ 'clear', + 'create', 'delete', + 'remove', ], rawData: [ true, @@ -596,7 +612,7 @@ export class GoogleSheets implements INodeType { { name: 'Formula', value: 'FORMULA', - description: ' Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".', + description: 'Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1".', }, { name: 'Unformatted Value', @@ -793,6 +809,170 @@ export class GoogleSheets implements INodeType { }, ], }, + + // ---------------------------------- + // sheet:create + // ---------------------------------- + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + default: true, + displayOptions: { + show: { + resource: [ + 'sheet', + ], + operation: [ + 'create', + ], + }, + }, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'sheet', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Grid Properties', + name: 'gridProperties', + type: 'collection', + placeholder: 'Add Property', + default: '', + options: [ + { + displayName: 'Column Count', + name: 'columnCount', + type: 'number', + default: 0, + description: 'The number of columns in the grid.', + }, + { + displayName: 'Column Group Control After', + name: 'columnGroupControlAfter', + type: 'boolean', + default: false, + description: 'True if the column grouping control toggle is shown after the group.', + }, + { + displayName: 'Frozen Column Count', + name: 'frozenColumnCount', + type: 'number', + default: 0, + description: 'The number of columns that are frozen in the grid.', + }, + { + displayName: 'Frozen Row Count', + name: 'frozenRowCount', + type: 'number', + default: 0, + description: 'The number of rows that are frozen in the grid.', + }, + { + displayName: 'Hide Gridlines', + name: 'hideGridlines', + type: 'boolean', + default: false, + description: 'True if the grid isn\'t showing gridlines in the UI.', + }, + { + displayName: 'Row Count', + name: 'rowCount', + type: 'number', + default: 0, + description: 'The number of rows in the grid.', + }, + { + displayName: 'Row Group Control After', + name: 'rowGroupControlAfter', + type: 'boolean', + default: false, + description: 'True if the row grouping control toggle is shown after the group.', + }, + + ], + description: 'The type of the sheet.', + }, + { + displayName: 'Hidden', + name: 'hidden', + type: 'boolean', + default: false, + description: 'True if the sheet is hidden in the UI, false if it\'s visible.', + }, + { + displayName: 'Right To Left', + name: 'rightToLeft', + type: 'boolean', + default: false, + description: 'True if the sheet is an RTL sheet instead of an LTR sheet.', + }, + { + displayName: 'Sheet ID', + name: 'sheetId', + type: 'number', + default: 0, + description: 'The ID of the sheet. Must be non-negative. This field cannot be changed once set.', + }, + { + displayName: 'Sheet Index', + name: 'index', + type: 'number', + default: 0, + description: 'The index of the sheet within the spreadsheet.', + }, + { + displayName: 'Tab Color', + name: 'tabColor', + type: 'color', + default: '0aa55c', + description: 'The color of the tab in the UI.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'The Sheet name.', + }, + ], + }, + + // ---------------------------------- + // sheet:remove + // ---------------------------------- + { + displayName: 'Sheet ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'sheet', + ], + operation: [ + 'remove', + ], + }, + }, + description: 'The ID of the sheet to delete.', + }, ], }; @@ -840,7 +1020,7 @@ export class GoogleSheets implements INodeType { const sheet = new GoogleSheet(spreadsheetId, this); let range = ''; - if (operation !== 'delete') { + if (!['create', 'delete', 'remove'].includes(operation)) { range = this.getNodeParameter('range', 0) as string; } @@ -878,6 +1058,39 @@ export class GoogleSheets implements INodeType { const items = this.getInputData(); return this.prepareOutputData(items); + + } else if (operation === 'create') { + const returnData: IDataObject[] = []; + + let responseData; + for (let i = 0; i < this.getInputData().length; i++) { + const spreadsheetId = this.getNodeParameter('sheetId', i) as string; + const options = this.getNodeParameter('options', i, {}) as IDataObject; + const simple = this.getNodeParameter('simple', 0) as boolean; + const properties = { ...options }; + + if (options.tabColor) { + const { red, green, blue } = hexToRgb(options.tabColor as string)!; + properties.tabColor = { red: red / 255, green: green / 255, blue: blue / 255 }; + } + + const requests = [{ + addSheet: { + properties, + }, + }]; + + responseData = await googleApiRequest.call(this, 'POST', `/v4/spreadsheets/${spreadsheetId}:batchUpdate`, { requests }); + + if (simple === true) { + Object.assign(responseData, responseData.replies[0].addSheet.properties); + delete responseData.replies; + } + returnData.push(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } else if (operation === 'delete') { // ---------------------------------- // delete @@ -976,6 +1189,27 @@ export class GoogleSheets implements INodeType { returnData = [{}]; } + return [this.helpers.returnJsonArray(returnData)]; + + } else if (operation === 'remove') { + const returnData: IDataObject[] = []; + + let responseData; + for (let i = 0; i < this.getInputData().length; i++) { + const sheetId = this.getNodeParameter('id', i) as string; + const spreadsheetId = this.getNodeParameter('sheetId', i) as string; + + const requests = [{ + deleteSheet: { + sheetId, + }, + }]; + + responseData = await googleApiRequest.call(this, 'POST', `/v4/spreadsheets/${spreadsheetId}:batchUpdate`, { requests }); + delete responseData.replies; + returnData.push(responseData); + } + return [this.helpers.returnJsonArray(returnData)]; } else if (operation === 'update') { // ---------------------------------- diff --git a/packages/nodes-base/nodes/Google/Sheet/googleSheets.svg b/packages/nodes-base/nodes/Google/Sheet/googleSheets.svg new file mode 100644 index 0000000000..b09ceb2ac8 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/googleSheets.svg @@ -0,0 +1,20 @@ + + + google_sheets_node_icon + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/Sheet/googlesheets.png b/packages/nodes-base/nodes/Google/Sheet/googlesheets.png deleted file mode 100644 index 3d46979127..0000000000 Binary files a/packages/nodes-base/nodes/Google/Sheet/googlesheets.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts new file mode 100644 index 0000000000..e25ec49e48 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Slides/GenericFunctions.ts @@ -0,0 +1,111 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + +export async function googleApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; + const options: OptionsWithUri & { headers: IDataObject } = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: `https://slides.googleapis.com/v1${resource}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + try { + if (authenticationMethod === 'serviceAccount') { + const credentials = this.getCredentials('googleApi') as { access_token: string, email: string, privateKey: string }; + const { access_token } = await getAccessToken.call(this, credentials); + options.headers.Authorization = `Bearer ${access_token}`; + return await this.helpers.request!(options); + + } else { + return await this.helpers.requestOAuth2!.call(this, 'googleSlidesOAuth2Api', options); + } + } catch (error) { + + if (error?.response?.body?.message) { + throw new Error(`Google Slides error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +function getAccessToken( + this: IExecuteFunctions | ILoadOptionsFunctions, + { email, privateKey }: { email: string, privateKey: string }, +) { + // https://developers.google.com/identity/protocols/oauth2/service-account#httprest + + const scopes = [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/presentations', + ]; + + const now = moment().unix(); + + const signature = jwt.sign( + { + iss: email, + sub: email, + scope: scopes.join(' '), + aud: 'https://oauth2.googleapis.com/token', + iat: now, + exp: now + 3600, + }, + privateKey, + { + algorithm: 'RS256', + header: { + kid: privateKey, + typ: 'JWT', + alg: 'RS256', + }, + }, + ); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + form: { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: signature, + }, + uri: 'https://oauth2.googleapis.com/token', + json: true, + }; + + return this.helpers.request!(options); +} diff --git a/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts new file mode 100644 index 0000000000..d2de0bf6f8 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts @@ -0,0 +1,554 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + googleApiRequest, +} from './GenericFunctions'; + +export class GoogleSlides implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Slides', + name: 'googleSlides', + icon: 'file:googleslides.svg', + group: ['input', 'output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Google Slides API', + defaults: { + name: 'Google Slides', + color: '#edba25', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, + { + name: 'googleSlidesOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'OAuth2', + value: 'oAuth2', + }, + { + name: 'Service Account', + value: 'serviceAccount', + }, + ], + default: 'serviceAccount', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Page', + value: 'page', + }, + { + name: 'Presentation', + value: 'presentation', + }, + ], + default: 'presentation', + description: 'Resource to operate on', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a presentation', + }, + { + name: 'Get', + value: 'get', + description: 'Get a presentation', + }, + { + name: 'Get Slides', + value: 'getSlides', + description: 'Get presentation slides', + }, + { + name: 'Replace Text', + value: 'replaceText', + description: 'Replace text in a presentation', + }, + ], + displayOptions: { + show: { + resource: [ + 'presentation', + ], + }, + }, + default: 'create', + description: 'Operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a page', + }, + { + name: 'Get Thumbnail', + value: 'getThumbnail', + description: 'Get a thumbnail', + }, + ], + displayOptions: { + show: { + resource: [ + 'page', + ], + }, + }, + default: 'get', + description: 'Operation to perform', + }, + { + displayName: 'Title', + name: 'title', + description: 'Title of the presentation to create.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'presentation', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Presentation ID', + name: 'presentationId', + description: 'ID of the presentation to retrieve. Found in the presentation URL:
https://docs.google.com/presentation/d/PRESENTATION_ID/edit', + placeholder: '1wZtNFZ8MO-WKrxhYrOLMvyiqSgFwdSz5vn8_l_7eNqw', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'presentation', + 'page', + ], + operation: [ + 'get', + 'getThumbnail', + 'getSlides', + 'replaceText', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getSlides', + ], + resource: [ + 'presentation', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getSlides', + ], + resource: [ + 'presentation', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Page Object ID', + name: 'pageObjectId', + description: 'ID of the page object to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'get', + 'getThumbnail', + ], + }, + }, + }, + { + displayName: 'Texts To Replace', + name: 'textUi', + placeholder: 'Add Text', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'presentation', + ], + operation: [ + 'replaceText', + ], + }, + }, + default: {}, + options: [ + { + name: 'textValues', + displayName: 'Text', + values: [ + { + displayName: 'Match Case', + name: 'matchCase', + type: 'boolean', + default: false, + description: 'Indicates whether the search should respect case. True : the search is case sensitive. False : the search is case insensitive.', + }, + { + displayName: 'Page IDs', + name: 'pageObjectIds', + type: 'multiOptions', + default: [], + typeOptions: { + loadOptionsMethod: 'getPages', + loadOptionsDependsOn: [ + 'presentationId', + ], + }, + description: 'If non-empty, limits the matches to page elements only on the given pages.', + }, + { + displayName: 'Replace Text', + name: 'replaceText', + type: 'string', + default: '', + description: 'The text that will replace the matched text.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text to search for in the shape or table.', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + operation: [ + 'replaceText', + ], + resource: [ + 'presentation', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Revision ID', + name: 'revisionId', + type: 'string', + default: '', + description: `The revision ID of the presentation required for the write request.
+ If specified and the requiredRevisionId doesn't exactly match the presentation's
+ current revisionId, the request will not be processed and will return a 400 bad request error.`, + }, + ], + }, + + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'getThumbnail', + ], + }, + }, + description: 'Name of the binary property to which to
write the data of the read page.', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'getThumbnail', + ], + download: [ + true, + ], + }, + }, + }, + ], + }; + + methods = { + loadOptions: { + // Get all the pages to display them to user so that he can + // select them easily + async getPages( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const presentationId = this.getCurrentNodeParameter('presentationId') as string; + const { slides } = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}`, {}, { fields: 'slides' }); + for (const slide of slides) { + returnData.push({ + name: slide.objectId, + value: slide.objectId, + }); + } + return returnData; + }, + }, + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + + if (resource === 'page') { + + // ********************************************************************* + // page + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // page: get + // ---------------------------------- + + const presentationId = this.getNodeParameter('presentationId', i) as string; + const pageObjectId = this.getNodeParameter('pageObjectId', i) as string; + responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}/pages/${pageObjectId}`); + returnData.push({ json: responseData }); + + } else if (operation === 'getThumbnail') { + + // ---------------------------------- + // page: getThumbnail + // ---------------------------------- + + const presentationId = this.getNodeParameter('presentationId', i) as string; + const pageObjectId = this.getNodeParameter('pageObjectId', i) as string; + responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}/pages/${pageObjectId}/thumbnail`); + + const download = this.getNodeParameter('download', 0) as boolean; + if (download === true) { + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + + const data = await this.helpers.request({ + uri: responseData.contentUrl, + method: 'GET', + json: false, + encoding: null, + }); + + const fileName = pageObjectId + '.png'; + const binaryData = await this.helpers.prepareBinaryData(data, fileName || fileName); + returnData.push({ + json: responseData, + binary: { + [binaryProperty]: binaryData, + }, + }); + } else { + returnData.push({ json: responseData }); + } + } + + } else if (resource === 'presentation') { + + // ********************************************************************* + // presentation + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // presentation: create + // ---------------------------------- + + const body = { + title: this.getNodeParameter('title', i) as string, + }; + + responseData = await googleApiRequest.call(this, 'POST', '/presentations', body); + returnData.push({ json: responseData }); + + } else if (operation === 'get') { + + // ---------------------------------- + // presentation: get + // ---------------------------------- + + const presentationId = this.getNodeParameter('presentationId', i) as string; + responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}`); + returnData.push({ json: responseData }); + + } else if (operation === 'getSlides') { + + // ---------------------------------- + // presentation: getSlides + // ---------------------------------- + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const presentationId = this.getNodeParameter('presentationId', i) as string; + responseData = await googleApiRequest.call(this, 'GET', `/presentations/${presentationId}`, {}, { fields: 'slides' }); + responseData = responseData.slides; + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + returnData.push(...this.helpers.returnJsonArray(responseData)); + + } else if (operation === 'replaceText') { + + // ---------------------------------- + // presentation: replaceText + // ---------------------------------- + const presentationId = this.getNodeParameter('presentationId', i) as string; + const texts = this.getNodeParameter('textUi.textValues', i, []) as IDataObject[]; + const options = this.getNodeParameter('options', i) as IDataObject; + const requests = texts.map((text => { + return { + replaceAllText: { + replaceText: text.replaceText, + pageObjectIds: text.pageObjectIds || [], + containsText: { + text: text.text, + matchCase: text.matchCase, + }, + }, + }; + })); + + const body: IDataObject = { + requests, + }; + + if (options.revisionId) { + body['writeControl'] = { + requiredRevisionId: options.revisionId as string, + }; + } + + responseData = await googleApiRequest.call(this, 'POST', `/presentations/${presentationId}:batchUpdate`, { requests }); + returnData.push({ json: responseData }); + + } + } + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Google/Slides/googleslides.svg b/packages/nodes-base/nodes/Google/Slides/googleslides.svg new file mode 100644 index 0000000000..d1fcb9c20b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Slides/googleslides.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts index 95c59ebeff..167eda8e67 100644 --- a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts +++ b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts @@ -236,7 +236,7 @@ export class GraphQL implements INodeType { requestOptions.body = { query: gqlQuery, variables: this.getNodeParameter('variables', itemIndex, {}) as object, - operationName: this.getNodeParameter('operationName', itemIndex, null) as string, + operationName: this.getNodeParameter('operationName', itemIndex) as string, }; if (typeof requestOptions.body.variables === 'string') { try { diff --git a/packages/nodes-base/nodes/Harvest/ClientDescription.ts b/packages/nodes-base/nodes/Harvest/ClientDescription.ts index 482f04d4d3..fef5a88f93 100644 --- a/packages/nodes-base/nodes/Harvest/ClientDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ClientDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['client']; +const resource = [ + 'client', +]; export const clientOperations = [ { @@ -49,7 +53,7 @@ export const clientOperations = [ export const clientFields = [ /* -------------------------------------------------------------------------- */ - /* client:getAll */ + /* client:getAll */ /* -------------------------------------------------------------------------- */ { @@ -122,7 +126,7 @@ export const clientFields = [ }, /* -------------------------------------------------------------------------- */ - /* client:get */ + /* client:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Client Id', @@ -142,7 +146,7 @@ export const clientFields = [ }, /* -------------------------------------------------------------------------- */ - /* client:delete */ + /* client:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Client Id', @@ -162,7 +166,7 @@ export const clientFields = [ }, /* -------------------------------------------------------------------------- */ - /* client:create */ + /* client:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Name', @@ -195,13 +199,6 @@ export const clientFields = [ }, default: {}, options: [ - { - displayName: 'Is Active', - name: 'is_active', - type: 'string', - default: '', - description: 'Whether the client is active, or archived. Defaults to true.', - }, { displayName: 'Address', name: 'address', @@ -216,11 +213,18 @@ export const clientFields = [ default: '', description: 'The currency used by the estimate. If not provided, the client’s currency will be used. See a list of supported currencies', }, + { + displayName: 'Is Active', + name: 'is_active', + type: 'string', + default: '', + description: 'Whether the client is active, or archived. Defaults to true.', + }, ], }, /* -------------------------------------------------------------------------- */ - /* client:update */ + /* client:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Client Id', diff --git a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts index 917b29deb6..64948b0a15 100644 --- a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts +++ b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['company']; +const resource = [ + 'company', +]; export const companyOperations = [ { diff --git a/packages/nodes-base/nodes/Harvest/ContactDescription.ts b/packages/nodes-base/nodes/Harvest/ContactDescription.ts index 3c17745e31..ac38607256 100644 --- a/packages/nodes-base/nodes/Harvest/ContactDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ContactDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['contact']; +const resource = [ + 'contact', +]; export const contactOperations = [ { @@ -48,7 +52,7 @@ export const contactOperations = [ export const contactFields = [ /* -------------------------------------------------------------------------- */ - /* contact:getAll */ + /* contact:getAll */ /* -------------------------------------------------------------------------- */ { @@ -121,7 +125,7 @@ export const contactFields = [ }, /* -------------------------------------------------------------------------- */ - /* contact:get */ + /* contact:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Contact Id', @@ -141,7 +145,7 @@ export const contactFields = [ }, /* -------------------------------------------------------------------------- */ - /* contact:delete */ + /* contact:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Contact Id', @@ -161,7 +165,7 @@ export const contactFields = [ }, /* -------------------------------------------------------------------------- */ - /* contact:create */ + /* contact:create */ /* -------------------------------------------------------------------------- */ { displayName: 'First Name', @@ -210,20 +214,6 @@ export const contactFields = [ }, default: {}, options: [ - { - displayName: 'Last Name', - name: 'last_name', - type: 'string', - default: '', - description: 'The last name of the contact.', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'The title of the contact.', - }, { displayName: 'Email', name: 'email', @@ -232,11 +222,18 @@ export const contactFields = [ description: 'The contact’s email address.', }, { - displayName: 'Phone Office', - name: 'phone_office', + displayName: 'Fax', + name: 'fax', type: 'string', default: '', - description: 'The contact’s office phone number.', + description: 'The contact’s fax number.', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'The last name of the contact.', }, { displayName: 'Phone Mobile', @@ -246,17 +243,25 @@ export const contactFields = [ description: 'The contact’s mobile phone number.', }, { - displayName: 'Fax', - name: 'fax', + displayName: 'Phone Office', + name: 'phone_office', type: 'string', default: '', - description: 'The contact’s fax number.', + description: 'The contact’s office phone number.', + }, + + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'The title of the contact.', }, ], }, /* -------------------------------------------------------------------------- */ - /* contact:update */ + /* contact:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Contact Id', @@ -296,6 +301,20 @@ export const contactFields = [ default: '', description: 'The ID of the client associated with this contact.', }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The contact’s email address.', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'The contact’s fax number.', + }, { displayName: 'First Name', name: 'first_name', @@ -311,18 +330,11 @@ export const contactFields = [ description: 'The last name of the contact.', }, { - displayName: 'Title', - name: 'title', + displayName: 'Phone Mobile', + name: 'phone_mobile', type: 'string', default: '', - description: 'The title of the contact.', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - default: '', - description: 'The contact’s email address.', + description: 'The contact’s mobile phone number.', }, { displayName: 'Phone Office', @@ -332,18 +344,11 @@ export const contactFields = [ description: 'The contact’s office phone number.', }, { - displayName: 'Phone Mobile', - name: 'phone_mobile', + displayName: 'Title', + name: 'title', type: 'string', default: '', - description: 'The contact’s mobile phone number.', - }, - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - description: 'The contact’s fax number.', + description: 'The title of the contact.', }, ], }, diff --git a/packages/nodes-base/nodes/Harvest/EstimateDescription.ts b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts index f827a27d8b..dbf918ba1c 100644 --- a/packages/nodes-base/nodes/Harvest/EstimateDescription.ts +++ b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['estimate']; +const resource = [ + 'estimate', +]; export const estimateOperations = [ { @@ -48,7 +52,7 @@ export const estimateOperations = [ export const estimateFields = [ /* -------------------------------------------------------------------------- */ - /* estimate:getAll */ + /* estimate:getAll */ /* -------------------------------------------------------------------------- */ { @@ -152,7 +156,7 @@ export const estimateFields = [ }, /* -------------------------------------------------------------------------- */ - /* estimate:get */ + /* estimate:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Estimate Id', @@ -172,7 +176,7 @@ export const estimateFields = [ }, /* -------------------------------------------------------------------------- */ - /* estimate:delete */ + /* estimate:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Estimate Id', @@ -192,7 +196,7 @@ export const estimateFields = [ }, /* -------------------------------------------------------------------------- */ - /* estimate:create */ + /* estimate:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Client Id', @@ -292,7 +296,7 @@ export const estimateFields = [ }, /* -------------------------------------------------------------------------- */ - /* estimate:update */ + /* estimate:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Invoice Id', diff --git a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts index a32e836c4b..024fa25059 100644 --- a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['expense']; +const resource = [ + 'expense', +]; export const expenseOperations = [ { @@ -48,7 +52,7 @@ export const expenseOperations = [ export const expenseFields = [ /* -------------------------------------------------------------------------- */ - /* expense:getAll */ + /* expense:getAll */ /* -------------------------------------------------------------------------- */ { @@ -166,7 +170,7 @@ export const expenseFields = [ }, /* -------------------------------------------------------------------------- */ - /* expense:get */ + /* expense:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Expense Id', @@ -186,7 +190,7 @@ export const expenseFields = [ }, /* -------------------------------------------------------------------------- */ - /* expense:delete */ + /* expense:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Expense Id', @@ -206,7 +210,7 @@ export const expenseFields = [ }, /* -------------------------------------------------------------------------- */ - /* expense:create */ + /* expense:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Project Id', @@ -310,7 +314,7 @@ export const expenseFields = [ }, /* -------------------------------------------------------------------------- */ - /* invoice:update */ + /* invoice:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Invoice Id', diff --git a/packages/nodes-base/nodes/Harvest/Harvest.node.ts b/packages/nodes-base/nodes/Harvest/Harvest.node.ts index b7edc1a7d5..5874355607 100644 --- a/packages/nodes-base/nodes/Harvest/Harvest.node.ts +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -54,6 +54,7 @@ import { taskFields, taskOperations, } from './TaskDescription'; + import { timeEntryFields, timeEntryOperations, @@ -174,17 +175,6 @@ export class Harvest implements INodeType { description: 'The resource to operate on.', }, - { - displayName: 'Account ID', - name: 'accountId', - type: 'options', - required: true, - typeOptions: { - loadOptionsMethod: 'getAccounts', - }, - default: '', - }, - // operations ...clientOperations, ...companyOperations, @@ -197,6 +187,17 @@ export class Harvest implements INodeType { ...timeEntryOperations, ...userOperations, + { + displayName: 'Account ID', + name: 'accountId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + default: '', + }, + // fields ...clientFields, ...contactFields, @@ -693,6 +694,37 @@ export class Harvest implements INodeType { const responseData: IDataObject[] = await getAllResource.call(this, 'tasks', i); returnData.push.apply(returnData, responseData); + } else if (operation === 'create') { + // ---------------------------------- + // create + // ---------------------------------- + + requestMethod = 'POST'; + endpoint = 'tasks'; + + body.name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(body, additionalFields); + + const responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint, body); + returnData.push(responseData); + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + requestMethod = 'PATCH'; + const id = this.getNodeParameter('id', i) as string; + endpoint = `tasks/${id}`; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(qs, updateFields); + + const responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint, body); + returnData.push(responseData); + } else if (operation === 'delete') { // ---------------------------------- // delete diff --git a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts index 4778507a1a..263636b08d 100644 --- a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts +++ b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['invoice']; +const resource = [ + 'invoice', +]; export const invoiceOperations = [ { @@ -48,7 +52,7 @@ export const invoiceOperations = [ export const invoiceFields = [ /* -------------------------------------------------------------------------- */ - /* invoice:getAll */ + /* invoice:getAll */ /* -------------------------------------------------------------------------- */ { @@ -110,20 +114,6 @@ export const invoiceFields = [ default: '', description: 'Only return time entries belonging to the client with the given ID.', }, - { - displayName: 'Project ID', - name: 'project_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'Updated Since', - name: 'updated_since', - type: 'dateTime', - default: '', - description: 'Only return time entries that have been updated since the given date and time.', - }, { displayName: 'From', name: 'from', @@ -132,11 +122,21 @@ export const invoiceFields = [ description: 'Only return time entries with a spent_date on or after the given date.', }, { - displayName: 'To', - name: 'to', - type: 'dateTime', + displayName: 'Page', + name: 'page', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + }, + { + displayName: 'Project ID', + name: 'project_id', + type: 'string', default: '', - description: 'Only return time entries with a spent_date on or before the given date.', + description: 'Only return time entries belonging to the client with the given ID.', }, { displayName: 'State', @@ -164,20 +164,25 @@ export const invoiceFields = [ description: 'Only return invoices with a state matching the value provided. Options: draft, open, paid, or closed.', }, { - displayName: 'Page', - name: 'page', - type: 'number', - typeOptions: { - minValue: 1, - }, - default: 1, - description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or before the given date.', + }, + + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return time entries that have been updated since the given date and time.', }, ], }, /* -------------------------------------------------------------------------- */ - /* invoice:get */ + /* invoice:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Invoice Id', @@ -197,7 +202,7 @@ export const invoiceFields = [ }, /* -------------------------------------------------------------------------- */ - /* invoice:delete */ + /* invoice:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Invoice Id', @@ -217,7 +222,7 @@ export const invoiceFields = [ }, /* -------------------------------------------------------------------------- */ - /* invoice:create */ + /* invoice:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Client Id', @@ -345,7 +350,7 @@ export const invoiceFields = [ }, /* -------------------------------------------------------------------------- */ - /* invoice:update */ + /* invoice:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Invoice Id', diff --git a/packages/nodes-base/nodes/Harvest/ProjectDescription.ts b/packages/nodes-base/nodes/Harvest/ProjectDescription.ts index 6a4b0d4202..067d94f111 100644 --- a/packages/nodes-base/nodes/Harvest/ProjectDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ProjectDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['project']; +const resource = [ + 'project', +]; export const projectOperations = [ { @@ -48,7 +52,7 @@ export const projectOperations = [ export const projectFields = [ /* -------------------------------------------------------------------------- */ - /* projects:getAll */ + /* projects:getAll */ /* -------------------------------------------------------------------------- */ { @@ -138,7 +142,7 @@ export const projectFields = [ }, /* -------------------------------------------------------------------------- */ - /* project:get */ + /* project:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Project Id', @@ -158,7 +162,7 @@ export const projectFields = [ }, /* -------------------------------------------------------------------------- */ - /* project:delete */ + /* project:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Project Id', @@ -178,7 +182,7 @@ export const projectFields = [ }, /* -------------------------------------------------------------------------- */ - /* project:create */ + /* project:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Name', @@ -399,7 +403,7 @@ export const projectFields = [ }, /* -------------------------------------------------------------------------- */ - /* project:update */ + /* project:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Project Id', @@ -576,7 +580,6 @@ export const projectFields = [ default: false, description: 'Option to show project budget to all employees. Does not apply to Total Project Fee projects. Defaults to false.', }, - { displayName: 'Starts On', name: 'starts_on', diff --git a/packages/nodes-base/nodes/Harvest/TaskDescription.ts b/packages/nodes-base/nodes/Harvest/TaskDescription.ts index a4926d70d1..c5b89773b1 100644 --- a/packages/nodes-base/nodes/Harvest/TaskDescription.ts +++ b/packages/nodes-base/nodes/Harvest/TaskDescription.ts @@ -1,5 +1,11 @@ -import { INodeProperties } from 'n8n-workflow'; -const resource = ['task']; +import { + INodeProperties +} from 'n8n-workflow'; + +const resource = [ + 'task', +]; + export const taskOperations = [ { displayName: 'Operation', @@ -46,7 +52,7 @@ export const taskOperations = [ export const taskFields = [ /* -------------------------------------------------------------------------- */ - /* task:getAll */ + /* task:getAll */ /* -------------------------------------------------------------------------- */ { displayName: 'Return All', @@ -128,7 +134,7 @@ export const taskFields = [ }, /* -------------------------------------------------------------------------- */ - /* task:get */ + /* task:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Task Id', @@ -148,7 +154,7 @@ export const taskFields = [ }, /* -------------------------------------------------------------------------- */ - /* task:delete */ + /* task:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Task Id', @@ -164,11 +170,11 @@ export const taskFields = [ resource, }, }, - description: 'The ID of the task you wan to delete.', + description: 'The ID of the task you want to delete.', }, /* -------------------------------------------------------------------------- */ - /* task:create */ + /* task:create */ /* -------------------------------------------------------------------------- */ { displayName: 'Name', @@ -205,23 +211,16 @@ export const taskFields = [ displayName: 'Billable By Default', name: 'billable_by_default', type: 'boolean', - default: '', + default: true, description: 'Used in determining whether default tasks should be marked billable when creating a new project. Defaults to true.', }, { displayName: 'Default Hourly Rate', name: 'default_hourly_rate', - type: 'string', - default: '0', + type: 'number', + default: 0, description: 'The default hourly rate to use for this task when it is added to a project. Defaults to 0.', }, - { - displayName: 'Is Default', - name: 'is_default', - type: 'boolean', - default: false, - description: 'Whether this task should be automatically added to future projects. Defaults to false.', - }, { displayName: 'Is Active', name: 'is_active', @@ -229,11 +228,35 @@ export const taskFields = [ default: true, description: 'Whether this task is active or archived. Defaults to true', }, + { + displayName: 'Is Default', + name: 'is_default', + type: 'boolean', + default: false, + description: 'Whether this task should be automatically added to future projects. Defaults to false.', + }, ], }, + /* -------------------------------------------------------------------------- */ - /* task:update */ + /* task:update */ /* -------------------------------------------------------------------------- */ + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource, + }, + }, + description: 'The ID of the task you want to update.', + }, { displayName: 'Update Fields', name: 'updateFields', @@ -260,8 +283,8 @@ export const taskFields = [ { displayName: 'Default Hourly Rate', name: 'default_hourly_rate', - type: 'string', - default: '0', + type: 'number', + default: 0, description: 'The default hourly rate to use for this task when it is added to a project. Defaults to 0.', }, { diff --git a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts index b4c40e8ce9..58ed292ace 100644 --- a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts +++ b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts @@ -1,5 +1,11 @@ -import { INodeProperties } from 'n8n-workflow'; -export const resource = ['timeEntry']; +import { + INodeProperties, +} from 'n8n-workflow'; + +export const resource = [ + 'timeEntry', +]; + export const timeEntryOperations = [ { displayName: 'Operation', @@ -147,6 +153,16 @@ export const timeEntryFields = [ default: true, description: 'Pass true to only return running time entries and false to return non-running time entries.', }, + { + displayName: 'Page', + name: 'page', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + }, { displayName: 'To', name: 'to', @@ -161,16 +177,6 @@ export const timeEntryFields = [ default: '', description: 'Only return time entries that have been updated since the given date and time.', }, - { - displayName: 'Page', - name: 'page', - type: 'number', - typeOptions: { - minValue: 1, - }, - default: 1, - description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', - }, { displayName: 'User ID', name: 'user_id', diff --git a/packages/nodes-base/nodes/Harvest/UserDescription.ts b/packages/nodes-base/nodes/Harvest/UserDescription.ts index 0254825b14..d3e2640cf5 100644 --- a/packages/nodes-base/nodes/Harvest/UserDescription.ts +++ b/packages/nodes-base/nodes/Harvest/UserDescription.ts @@ -1,6 +1,10 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; -const resource = ['user']; +const resource = [ + 'user', +]; export const userOperations = [ { @@ -54,7 +58,7 @@ export const userOperations = [ export const userFields = [ /* -------------------------------------------------------------------------- */ - /* user:getAll */ + /* user:getAll */ /* -------------------------------------------------------------------------- */ { @@ -137,7 +141,7 @@ export const userFields = [ }, /* -------------------------------------------------------------------------- */ - /* user:get */ + /* user:get */ /* -------------------------------------------------------------------------- */ { displayName: 'User Id', @@ -157,7 +161,7 @@ export const userFields = [ }, /* -------------------------------------------------------------------------- */ - /* user:delete */ + /* user:delete */ /* -------------------------------------------------------------------------- */ { displayName: 'User Id', @@ -177,7 +181,7 @@ export const userFields = [ }, /* -------------------------------------------------------------------------- */ - /* user:create */ + /* user:create */ /* -------------------------------------------------------------------------- */ { displayName: 'First Name', @@ -242,13 +246,6 @@ export const userFields = [ }, default: {}, options: [ - { - displayName: 'Can Create Projects', - name: 'can_create_projects', - type: 'boolean', - default: false, - description: 'Whether the user can create projects. Only applicable to Project Managers.', - }, { displayName: 'Can Create Invoices', name: 'can_create_invoices', @@ -256,6 +253,13 @@ export const userFields = [ default: false, description: 'Whether the user can create invoices. Only applicable to Project Managers.', }, + { + displayName: 'Can Create Projects', + name: 'can_create_projects', + type: 'boolean', + default: false, + description: 'Whether the user can create projects. Only applicable to Project Managers.', + }, { displayName: 'Can See Rates', name: 'can_see_rates', @@ -342,9 +346,8 @@ export const userFields = [ ], }, - /* -------------------------------------------------------------------------- */ - /* user:update */ + /* user:update */ /* -------------------------------------------------------------------------- */ { displayName: 'Time Entry Id', @@ -377,13 +380,6 @@ export const userFields = [ }, default: {}, options: [ - { - displayName: 'Can Create Projects', - name: 'can_create_projects', - type: 'boolean', - default: false, - description: 'Whether the user can create projects. Only applicable to Project Managers.', - }, { displayName: 'Can Create Invoices', name: 'can_create_invoices', @@ -391,6 +387,13 @@ export const userFields = [ default: false, description: 'Whether the user can create invoices. Only applicable to Project Managers.', }, + { + displayName: 'Can Create Projects', + name: 'can_create_projects', + type: 'boolean', + default: false, + description: 'Whether the user can create projects. Only applicable to Project Managers.', + }, { displayName: 'Can See Rates', name: 'can_see_rates', diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts index 886cfa2b29..ffdde00d10 100644 --- a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -1,10 +1,14 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, } from 'n8n-core'; + import { IDataObject, } from 'n8n-workflow'; @@ -35,8 +39,8 @@ export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSing return await this.helpers.requestOAuth2.call(this, 'helpScoutOAuth2Api', options); } catch (error) { if (error.response && error.response.body - && error.response.body._embedded - && error.response.body._embedded.errors) { + && error.response.body._embedded + && error.response.body._embedded.errors) { // Try to return the error prettier //@ts-ignore throw new Error(`HelpScout error response [${error.statusCode}]: ${error.response.body.message} - ${error.response.body._embedded.errors.map(error => { @@ -48,7 +52,7 @@ export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSing } } -export async function helpscoutApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function helpscoutApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts index 4b72299179..54f2495682 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -59,7 +59,7 @@ export class HelpScout implements INodeType { description: INodeTypeDescription = { displayName: 'HelpScout', name: 'helpScout', - icon: 'file:helpScout.png', + icon: 'file:helpScout.svg', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -139,7 +139,7 @@ export class HelpScout implements INodeType { const tagId = tag.id; returnData.push({ name: tagName, - value: tagId, + value: tagName, }); } return returnData; @@ -391,23 +391,23 @@ export class HelpScout implements INodeType { } if (attachments) { if (attachments.attachmentsValues - && (attachments.attachmentsValues as IDataObject[]).length !== 0) { + && (attachments.attachmentsValues as IDataObject[]).length !== 0) { body.attachments?.push.apply(body.attachments, attachments.attachmentsValues as IAttachment[]); } if (attachments.attachmentsBinary - && (attachments.attachmentsBinary as IDataObject[]).length !== 0 - && items[i].binary) { - const mapFunction = (value: IDataObject): IAttachment => { - const binaryProperty = (items[i].binary as IBinaryKeyData)[value.property as string]; + && (attachments.attachmentsBinary as IDataObject[]).length !== 0 + && items[i].binary) { + const mapFunction = (value: IDataObject): IAttachment => { + const binaryProperty = (items[i].binary as IBinaryKeyData)[value.property as string]; if (binaryProperty) { return { fileName: binaryProperty.fileName || 'unknown', data: binaryProperty.data, mimeType: binaryProperty.mimeType, - }; - } else { - throw new Error(`Binary property ${value.property} does not exist on input`); - } + }; + } else { + throw new Error(`Binary property ${value.property} does not exist on input`); + } }; body.attachments?.push.apply(body.attachments, (attachments.attachmentsBinary as IDataObject[]).map(mapFunction) as IAttachment[]); } diff --git a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts index 63592aa14e..20a03de3f2 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts @@ -15,13 +15,15 @@ import { helpscoutApiRequestAllItems, } from './GenericFunctions'; -import { createHmac } from 'crypto'; +import { + createHmac, +} from 'crypto'; export class HelpScoutTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'HelpScout Trigger', name: 'helpScoutTrigger', - icon: 'file:helpScout.png', + icon: 'file:helpScout.svg', group: ['trigger'], version: 1, description: 'Starts the workflow when HelpScout events occure.', @@ -125,7 +127,7 @@ export class HelpScoutTrigger implements INodeType { if (webhook.url === webhookUrl) { for (const event of events) { if (!webhook.events.includes(event) - && webhook.state === 'enabled') { + && webhook.state === 'enabled') { return false; } } @@ -186,7 +188,7 @@ export class HelpScoutTrigger implements INodeType { const bodyData = this.getBodyData(); const headerData = this.getHeaderData() as IDataObject; const webhookData = this.getWorkflowStaticData('node'); - if (headerData['x-helpscout-signature'] === undefined) { + if (headerData['x-helpscout-signature'] === undefined) { return {}; } //@ts-ignore diff --git a/packages/nodes-base/nodes/HelpScout/helpScout.png b/packages/nodes-base/nodes/HelpScout/helpScout.png deleted file mode 100644 index 1ebdaa8e5e..0000000000 Binary files a/packages/nodes-base/nodes/HelpScout/helpScout.png and /dev/null differ diff --git a/packages/nodes-base/nodes/HelpScout/helpScout.svg b/packages/nodes-base/nodes/HelpScout/helpScout.svg new file mode 100644 index 0000000000..2645eaa983 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/helpScout.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 6c8225aaf9..dfd7dff69a 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -813,11 +813,14 @@ export class HttpRequest implements INodeType { if (responseFormat === 'file') { requestOptions.encoding = null; - requestOptions.body = JSON.stringify(requestOptions.body); - if (requestOptions.headers === undefined) { - requestOptions.headers = {}; + + if (options.bodyContentType !== 'raw') { + requestOptions.body = JSON.stringify(requestOptions.body); + if (requestOptions.headers === undefined) { + requestOptions.headers = {}; + } + requestOptions.headers['Content-Type'] = 'application/json'; } - requestOptions.headers['Content-Type'] = 'application/json'; } else if (options.bodyContentType === 'raw') { requestOptions.json = false; } else { @@ -826,7 +829,7 @@ export class HttpRequest implements INodeType { // Add Content Type if any are set if (options.bodyContentCustomMimeType) { - if(requestOptions.headers === undefined) { + if (requestOptions.headers === undefined) { requestOptions.headers = {}; } requestOptions.headers['Content-Type'] = options.bodyContentCustomMimeType; @@ -926,7 +929,7 @@ export class HttpRequest implements INodeType { if (property === 'body') { continue; } - returnItem[property] = response![property]; + returnItem[property] = response![property]; } newItem.json = returnItem; diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts index 7bd4597b8a..8c66afbd28 100644 --- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -170,6 +170,12 @@ export const dealFields = [ }, ], }, + { + displayName: 'Deal Description', + name: 'description', + type: 'string', + default: '', + }, { displayName: 'Deal Name', name: 'dealName', @@ -279,6 +285,12 @@ export const dealFields = [ }, ], }, + { + displayName: 'Deal Description', + name: 'description', + type: 'string', + default: '', + }, { displayName: 'Deal Name', name: 'dealName', diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index a5a05a562d..5aec7790d8 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -44,7 +44,7 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions return await this.helpers.request!(options); } else { // @ts-ignore - return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, { tokenType: 'Bearer' }); + return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, { tokenType: 'Bearer', includeCredentialsOnRefreshOnBody: true }); } } catch (error) { let errorMessages; diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index 3c82f2941e..b99fbaf0d7 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -1924,6 +1924,12 @@ export class Hubspot implements INodeType { value: additionalFields.pipeline as string, }); } + if (additionalFields.description) { + body.properties.push({ + name: 'description', + value: additionalFields.description as string, + }); + } if (additionalFields.customPropertiesUi) { const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; if (customProperties) { @@ -1980,6 +1986,12 @@ export class Hubspot implements INodeType { value: updateFields.pipeline as string, }); } + if (updateFields.description) { + body.properties.push({ + name: 'description', + value: updateFields.description as string, + }); + } if (updateFields.customPropertiesUi) { const customProperties = (updateFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; if (customProperties) { diff --git a/packages/nodes-base/nodes/Hubspot/hubspot.svg b/packages/nodes-base/nodes/Hubspot/hubspot.svg index 0fd4c993e1..7e1637cb5d 100644 --- a/packages/nodes-base/nodes/Hubspot/hubspot.svg +++ b/packages/nodes-base/nodes/Hubspot/hubspot.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/nodes-base/nodes/If.node.ts b/packages/nodes-base/nodes/If.node.ts index 1e35d43d41..ba7e5e4441 100644 --- a/packages/nodes-base/nodes/If.node.ts +++ b/packages/nodes-base/nodes/If.node.ts @@ -31,6 +31,7 @@ export class If implements INodeType { type: 'fixedCollection', typeOptions: { multipleValues: true, + sortable: true, }, description: 'The type of values to compare.', default: {}, @@ -72,6 +73,43 @@ export class If implements INodeType { }, ], }, + { + name: 'dateTime', + displayName: 'Date & Time', + values: [ + { + displayName: 'Value 1', + name: 'value1', + type: 'dateTime', + default: '', + description: 'The value to compare with the second one.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Occurred after', + value: 'after', + }, + { + name: 'Occurred before', + value: 'before', + }, + ], + default: 'after', + description: 'Operation to decide where the the data should be mapped to.', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'dateTime', + default: '', + description: 'The value to compare with the first one.', + }, + ], + }, { name: 'number', displayName: 'Number', @@ -258,6 +296,8 @@ export class If implements INodeType { const compareOperationFunctions: { [key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean; } = { + after: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0), + before: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0), contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()), notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()), endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string), @@ -285,9 +325,28 @@ export class If implements INodeType { }, }; + // Converts the input data of a dateTime into a number for easy compare + function convertDateTime(value: NodeParameterValue): number { + let returnValue: number | undefined = undefined; + if (typeof value === 'string') { + returnValue = new Date(value).getTime(); + } else if (typeof value === 'number') { + returnValue = value; + } if ((value as unknown as object) instanceof Date) { + returnValue = (value as unknown as Date).getTime(); + } + + if (returnValue === undefined || isNaN(returnValue)) { + throw new Error(`The value "${value}" is not a valid DateTime.`); + } + + return returnValue; + } + // The different dataTypes to check the values in const dataTypes = [ 'boolean', + 'dateTime', 'number', 'string', ]; @@ -296,6 +355,7 @@ export class If implements INodeType { // which ones via output "false" let dataType: string; let compareOperationResult: boolean; + let value1: NodeParameterValue, value2: NodeParameterValue; itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { item = items[itemIndex]; @@ -309,7 +369,16 @@ export class If implements INodeType { // Check all the values of the current dataType for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, []) as INodeParameters[]) { // Check if the values passes - compareOperationResult = compareOperationFunctions[compareData.operation as string](compareData.value1 as NodeParameterValue, compareData.value2 as NodeParameterValue); + + value1 = compareData.value1 as NodeParameterValue; + value2 = compareData.value2 as NodeParameterValue; + + if (dataType === 'dateTime') { + value1 = convertDateTime(value1); + value2 = convertDateTime(value2); + } + + compareOperationResult = compareOperationFunctions[compareData.operation as string](value1, value2); if (compareOperationResult === true && combineOperation === 'any') { // If it passes and the operation is "any" we do not have to check any diff --git a/packages/nodes-base/nodes/InvoiceNinja/GenericFunctions.ts b/packages/nodes-base/nodes/InvoiceNinja/GenericFunctions.ts index dc9fabeb0e..947be4d0c3 100644 --- a/packages/nodes-base/nodes/InvoiceNinja/GenericFunctions.ts +++ b/packages/nodes-base/nodes/InvoiceNinja/GenericFunctions.ts @@ -1,4 +1,6 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; import { IExecuteFunctions, @@ -11,7 +13,9 @@ import { IDataObject, } from 'n8n-workflow'; -import { get } from 'lodash'; +import { + get, +} from 'lodash'; export async function invoiceNinjaApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('invoiceNinjaApi'); diff --git a/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinja.node.ts b/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinja.node.ts index 3979e165cb..3327997f7f 100644 --- a/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinja.node.ts +++ b/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinja.node.ts @@ -1,6 +1,7 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, ILoadOptionsFunctions, @@ -9,63 +10,77 @@ import { INodeType, INodeTypeDescription, } from 'n8n-workflow'; + import { invoiceNinjaApiRequest, invoiceNinjaApiRequestAllItems, } from './GenericFunctions'; + import { clientFields, clientOperations, } from './ClientDescription'; + import { invoiceFields, invoiceOperations, } from './InvoiceDescription'; + import { IClient, IContact, } from './ClientInterface'; + import { countryCodes, - } from './ISOCountryCodes'; +} from './ISOCountryCodes'; + import { IInvoice, IItem, - } from './invoiceInterface'; +} from './invoiceInterface'; + import { taskFields, taskOperations, - } from './TaskDescription'; - import { +} from './TaskDescription'; + +import { ITask, - } from './TaskInterface'; - import { +} from './TaskInterface'; + +import { paymentFields, paymentOperations, - } from './PaymentDescription'; - import { +} from './PaymentDescription'; + +import { IPayment, - } from './PaymentInterface'; - import { +} from './PaymentInterface'; + +import { expenseFields, expenseOperations, - } from './ExpenseDescription'; - import { +} from './ExpenseDescription'; + +import { IExpense, - } from './ExpenseInterface'; - import { +} from './ExpenseInterface'; + +import { quoteFields, quoteOperations, - } from './QuoteDescription'; - import { +} from './QuoteDescription'; + +import { IQuote, - } from './QuoteInterface'; +} from './QuoteInterface'; export class InvoiceNinja implements INodeType { description: INodeTypeDescription = { displayName: 'Invoice Ninja', name: 'invoiceNinja', - icon: 'file:invoiceNinja.png', + icon: 'file:invoiceNinja.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -462,7 +477,7 @@ export class InvoiceNinja implements INodeType { body.client_id = additionalFields.client as number; } if (additionalFields.project) { - body.project = additionalFields.project as number; + body.project_id = additionalFields.project as number; } if (additionalFields.customValue1) { body.custom_value1 = additionalFields.customValue1 as string; @@ -479,10 +494,10 @@ export class InvoiceNinja implements INodeType { for (const logValue of timeLogsValues) { let from = 0, to; if (logValue.startDate) { - from = new Date(logValue.startDate as string).getTime()/1000 as number; + from = new Date(logValue.startDate as string).getTime() / 1000 as number; } if (logValue.endDate) { - to = new Date(logValue.endDate as string).getTime()/1000 as number; + to = new Date(logValue.endDate as string).getTime() / 1000 as number; } if (logValue.duration) { to = from + (logValue.duration as number * 3600); diff --git a/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts b/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts index 3833dddcfd..b8fbe79a38 100644 --- a/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts +++ b/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts @@ -17,7 +17,7 @@ export class InvoiceNinjaTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Invoice Ninja Trigger', name: 'invoiceNinjaTrigger', - icon: 'file:invoiceNinja.png', + icon: 'file:invoiceNinja.svg', group: ['trigger'], version: 1, description: 'Starts the workflow when Invoice Ninja events occure.', diff --git a/packages/nodes-base/nodes/InvoiceNinja/TaskInterface.ts b/packages/nodes-base/nodes/InvoiceNinja/TaskInterface.ts index 383bed5d94..62ef311a61 100644 --- a/packages/nodes-base/nodes/InvoiceNinja/TaskInterface.ts +++ b/packages/nodes-base/nodes/InvoiceNinja/TaskInterface.ts @@ -3,6 +3,6 @@ export interface ITask { custom_value1?: string; custom_value2?: string; description?: string; - project?: number; + project_id?: number; time_log?: string; } diff --git a/packages/nodes-base/nodes/InvoiceNinja/invoiceNinja.png b/packages/nodes-base/nodes/InvoiceNinja/invoiceNinja.png deleted file mode 100644 index 50c5ac69ca..0000000000 Binary files a/packages/nodes-base/nodes/InvoiceNinja/invoiceNinja.png and /dev/null differ diff --git a/packages/nodes-base/nodes/InvoiceNinja/invoiceNinja.svg b/packages/nodes-base/nodes/InvoiceNinja/invoiceNinja.svg new file mode 100644 index 0000000000..7ce814392f --- /dev/null +++ b/packages/nodes-base/nodes/InvoiceNinja/invoiceNinja.svg @@ -0,0 +1 @@ +Invoice Ninja icon \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index f67375477f..84ee2dae48 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -253,6 +253,16 @@ export const issueFields = [ default: '', description: 'Priority', }, + { + displayName: 'Reporter', + name: 'reporter', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'Reporter', + }, { displayName: 'Update History', name: 'updateHistory', @@ -418,6 +428,16 @@ export const issueFields = [ default: '', description: 'Priority', }, + { + displayName: 'Reporter', + name: 'reporter', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'Reporter', + }, { displayName: 'Summary', name: 'summary', diff --git a/packages/nodes-base/nodes/Jira/IssueInterface.ts b/packages/nodes-base/nodes/Jira/IssueInterface.ts index a3a75f2d59..bef90057a9 100644 --- a/packages/nodes-base/nodes/Jira/IssueInterface.ts +++ b/packages/nodes-base/nodes/Jira/IssueInterface.ts @@ -11,6 +11,7 @@ export interface IFields { priority?: IDataObject; project?: IDataObject; summary?: string; + reporter?: IDataObject; } export interface IIssue { diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index 6a89019268..eeb9d7c69d 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -43,6 +43,11 @@ import { NotificationRecipientsRestrictions, } from './IssueInterface'; +import { + userFields, + userOperations, +} from './UserDescription'; + export class Jira implements INodeType { description: INodeTypeDescription = { displayName: 'Jira Software', @@ -119,6 +124,11 @@ export class Jira implements INodeType { value: 'issueComment', description: 'Get, create, update, and delete a comment from an issue.', }, + { + name: 'User', + value: 'user', + description: 'Get, create and delete a user.', + }, ], default: 'issue', description: 'Resource to consume.', @@ -129,6 +139,8 @@ export class Jira implements INodeType { ...issueAttachmentFields, ...issueCommentOperations, ...issueCommentFields, + ...userOperations, + ...userFields, ], }; @@ -436,6 +448,11 @@ export class Jira implements INodeType { }; } } + if (additionalFields.reporter) { + fields.reporter = { + id: additionalFields.reporter as string, + }; + } if (additionalFields.description) { fields.description = additionalFields.description as string; } @@ -508,6 +525,11 @@ export class Jira implements INodeType { }; } } + if (updateFields.reporter) { + fields.reporter = { + id: updateFields.reporter as string, + }; + } if (updateFields.description) { fields.description = updateFields.description as string; } @@ -952,10 +974,51 @@ export class Jira implements INodeType { } } + if (resource === 'user') { + if (operation === 'create') { + // https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-post + for (let i = 0; i < length; i++) { + const body = { + name: this.getNodeParameter('username', i), + emailAddress: this.getNodeParameter('emailAddress', i), + displayName: this.getNodeParameter('displayName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + Object.assign(body, additionalFields); + + responseData = await jiraSoftwareCloudApiRequest.call(this, '/api/3/user', 'POST', body, {}); + returnData.push(responseData); + } + } else if (operation === 'delete') { + // https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-delete + for (let i = 0; i < length; i++) { + qs.accountId = this.getNodeParameter('accountId', i); + responseData = await jiraSoftwareCloudApiRequest.call(this, '/api/3/user', 'DELETE', {}, qs); + returnData.push({ success: true }); + } + } else if (operation === 'get') { + // https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-get + for (let i = 0; i < length; i++) { + qs.accountId = this.getNodeParameter('accountId', i); + + const { expand } = this.getNodeParameter('additionalFields', i) as { expand: string[] }; + + if (expand) { + qs.expand = expand.join(','); + } + + responseData = await jiraSoftwareCloudApiRequest.call(this, '/api/3/user', 'GET', {}, qs); + returnData.push(responseData); + } + } + } + if (resource === 'issueAttachment' && (operation === 'getAll' || operation === 'get')) { return this.prepareOutputData(returnData as unknown as INodeExecutionData[]); } else { return [this.helpers.returnJsonArray(returnData)]; } } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/Jira/UserDescription.ts b/packages/nodes-base/nodes/Jira/UserDescription.ts new file mode 100644 index 0000000000..11cb886535 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/UserDescription.ts @@ -0,0 +1,207 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new user.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a user.', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a user.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ + + /* -------------------------------------------------------------------------- */ + /* user:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email Address', + name: 'emailAddress', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + description: 'Password for the user. If a password is not set, a random password is generated.', + typeOptions: { + password: true, + }, + }, + { + displayName: 'Notification', + name: 'notification', + type: 'boolean', + default: false, + description: 'Send the user an email confirmation that they have been added to Jira.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* user:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + default: '', + description: 'Account ID of the user to delete.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'delete', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* user:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + default: '', + description: 'Account ID of the user to retrieve.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Expand', + name: 'expand', + type: 'multiOptions', + default: [], + description: 'Include more information about the user.', + options: [ + { + name: 'Groups', + value: 'groups', + description: 'Include all groups to which the user belongs.', + }, + { + name: 'Application Roles', + value: 'applicationRoles', + description: 'Include details of all the applications the user can access.', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Jira/jira.svg b/packages/nodes-base/nodes/Jira/jira.svg index c1ee7fe198..edf23f5aea 100644 --- a/packages/nodes-base/nodes/Jira/jira.svg +++ b/packages/nodes-base/nodes/Jira/jira.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/nodes-base/nodes/JotForm/JotFormTrigger.node.ts b/packages/nodes-base/nodes/JotForm/JotFormTrigger.node.ts index 755b2bd613..c379ce3254 100644 --- a/packages/nodes-base/nodes/JotForm/JotFormTrigger.node.ts +++ b/packages/nodes-base/nodes/JotForm/JotFormTrigger.node.ts @@ -167,7 +167,7 @@ export class JotFormTrigger implements INodeType { const resolveData = this.getNodeParameter('resolveData', false) as boolean; const onlyAnswers = this.getNodeParameter('onlyAnswers', false) as boolean; - const form = new formidable.IncomingForm(); + const form = new formidable.IncomingForm({}); return new Promise((resolve, reject) => { diff --git a/packages/nodes-base/nodes/Kafka/Kafka.node.ts b/packages/nodes-base/nodes/Kafka/Kafka.node.ts index 61f4268e24..2b7876a672 100644 --- a/packages/nodes-base/nodes/Kafka/Kafka.node.ts +++ b/packages/nodes-base/nodes/Kafka/Kafka.node.ts @@ -193,10 +193,14 @@ export class Kafka implements INodeType { ssl, }; - if (credentials.username || credentials.password) { + if (credentials.authentication === true) { + if(!(credentials.username && credentials.password)) { + throw new Error('Username and password are required for authentication'); + } config.sasl = { username: credentials.username as string, password: credentials.password as string, + mechanism: credentials.saslMechanism as string, } as SASLOptions; } diff --git a/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts b/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts index 3530b12a17..0c7e91abf4 100644 --- a/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts +++ b/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts @@ -123,10 +123,14 @@ export class KafkaTrigger implements INodeType { logLevel: logLevel.ERROR, }; - if (credentials.username || credentials.password) { + if (credentials.authentication === true) { + if(!(credentials.username && credentials.password)) { + throw new Error('Username and password are required for authentication'); + } config.sasl = { username: credentials.username as string, password: credentials.password as string, + mechanism: credentials.saslMechanism as string, } as SASLOptions; } diff --git a/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts b/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts new file mode 100644 index 0000000000..1618f50b07 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/GenericFunctions.ts @@ -0,0 +1,93 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to Lemlist. + */ +export async function lemlistApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + option: IDataObject = {}, +) { + + const { apiKey } = this.getCredentials('lemlistApi') as { + apiKey: string, + }; + + const encodedApiKey = Buffer.from(':' + apiKey).toString('base64'); + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'Authorization': `Basic ${encodedApiKey}`, + }, + method, + uri: `https://api.lemlist.com/api${endpoint}`, + qs, + body, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error?.response?.body) { + throw new Error(`Lemlist error response [${error.statusCode}]: ${error?.response?.body}`); + } + + throw error; + } +} + +/** + * Make an authenticated API request to Lemlist and return all results. + */ +export async function lemlistApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, + method: string, + endpoint: string, +) { + const returnData: IDataObject[] = []; + + let responseData; + const qs: IDataObject = {}; + + qs.limit = 100; + qs.offset = 0; + + do { + responseData = await lemlistApiRequest.call(this, method, endpoint, {}, qs); + returnData.push(...responseData); + qs.offset += qs.limit; + } while ( + responseData.length !== 0 + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts new file mode 100644 index 0000000000..8f5ec3d859 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts @@ -0,0 +1,337 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + activityFields, + activityOperations, + campaignFields, + campaignOperations, + leadFields, + leadOperations, + teamFields, + teamOperations, + unsubscribeFields, + unsubscribeOperations, +} from './descriptions'; + +import { + lemlistApiRequest, + lemlistApiRequestAllItems, +} from './GenericFunctions'; + +import { + isEmpty, + omit, +} from 'lodash'; + +export class Lemlist implements INodeType { + description: INodeTypeDescription = { + displayName: 'Lemlist', + name: 'lemlist', + icon: 'file:lemlist.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Lemlist API', + defaults: { + name: 'Lemlist', + color: '#4d19ff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'lemlistApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Campaign', + value: 'campaign', + }, + { + name: 'Lead', + value: 'lead', + }, + { + name: 'Team', + value: 'team', + }, + { + name: 'Unsubscribes', + value: 'unsubscribe', + }, + ], + default: 'activity', + description: 'Resource to consume', + }, + ...activityOperations, + ...activityFields, + ...campaignOperations, + ...campaignFields, + ...leadOperations, + ...leadFields, + ...teamOperations, + ...teamFields, + ...unsubscribeOperations, + ...unsubscribeFields, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions) { + const campaigns = await lemlistApiRequest.call(this, 'GET', '/campaigns'); + return campaigns.map(({ _id, name }: { _id: string, name: string }) => ({ + name, + value: _id, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'activity') { + + // ********************************************************************* + // activity + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // activity: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#activities + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i); + + if (!isEmpty(filters)) { + Object.assign(qs, filters); + } + + responseData = await lemlistApiRequest.call(this, 'GET', '/activities', {}, qs); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.slice(0, limit); + } + + } + + } else if (resource === 'campaign') { + + // ********************************************************************* + // campaign + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#list-all-campaigns + + responseData = await lemlistApiRequest.call(this, 'GET', '/campaigns'); + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i); + responseData = responseData.slice(0, limit); + } + } + + } else if (resource === 'lead') { + + // ********************************************************************* + // lead + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // lead: create + // ---------------------------------- + + // https://developer.lemlist.com/#add-a-lead-in-a-campaign + + const qs = {} as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.deduplicate !== undefined) { + qs.deduplicate = additionalFields.deduplicate; + } + + const body = {} as IDataObject; + + const remainingAdditionalFields = omit(additionalFields, 'deduplicate'); + + if (!isEmpty(remainingAdditionalFields)) { + Object.assign(body, remainingAdditionalFields); + } + + const campaignId = this.getNodeParameter('campaignId', i); + const email = this.getNodeParameter('email', i); + const endpoint = `/campaigns/${campaignId}/leads/${email}`; + + responseData = await lemlistApiRequest.call(this, 'POST', endpoint, body, qs); + + } else if (operation === 'delete') { + + // ---------------------------------- + // lead: delete + // ---------------------------------- + + // https://developer.lemlist.com/#delete-a-lead-from-a-campaign + + const campaignId = this.getNodeParameter('campaignId', i); + const email = this.getNodeParameter('email', i); + const endpoint = `/campaigns/${campaignId}/leads/${email}`; + responseData = await lemlistApiRequest.call(this, 'DELETE', endpoint, {}, { action: 'remove' }); + + } else if (operation === 'get') { + + // ---------------------------------- + // lead: get + // ---------------------------------- + + // https://developer.lemlist.com/#get-a-specific-lead-by-email + + const email = this.getNodeParameter('email', i); + responseData = await lemlistApiRequest.call(this, 'GET', `/leads/${email}`); + + } else if (operation === 'unsubscribe') { + + // ---------------------------------- + // lead: unsubscribe + // ---------------------------------- + + // https://developer.lemlist.com/#unsubscribe-a-lead-from-a-campaign + + const campaignId = this.getNodeParameter('campaignId', i); + const email = this.getNodeParameter('email', i); + const endpoint = `/campaigns/${campaignId}/leads/${email}`; + responseData = await lemlistApiRequest.call(this, 'DELETE', endpoint); + + } + + } else if (resource === 'team') { + + // ********************************************************************* + // team + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // team: get + // ---------------------------------- + + // https://developer.lemlist.com/#team + + responseData = await lemlistApiRequest.call(this, 'GET', '/team'); + + } + + } else if (resource === 'unsubscribe') { + + // ********************************************************************* + // unsubscribe + // ********************************************************************* + + if (operation === 'add') { + + // ---------------------------------- + // unsubscribe: Add + // ---------------------------------- + + // https://developer.lemlist.com/#add-an-email-address-in-the-unsubscribes + + const email = this.getNodeParameter('email', i); + responseData = await lemlistApiRequest.call(this, 'POST', `/unsubscribes/${email}`); + + } else if (operation === 'delete') { + + // ---------------------------------- + // unsubscribe: delete + // ---------------------------------- + + // https://developer.lemlist.com/#delete-an-email-address-from-the-unsubscribes + + const email = this.getNodeParameter('email', i); + responseData = await lemlistApiRequest.call(this, 'DELETE', `/unsubscribes/${email}`); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // unsubscribe: getAll + // ---------------------------------- + + // https://developer.lemlist.com/#list-all-unsubscribes + + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/unsubscribes'); + } else { + const qs = { + limit: this.getNodeParameter('limit', i) as number, + }; + responseData = await lemlistApiRequest.call(this, 'GET', '/unsubscribes', {}, qs); + } + } + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts b/packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts new file mode 100644 index 0000000000..bf671c1d5b --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/LemlistTrigger.node.ts @@ -0,0 +1,175 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + lemlistApiRequest, +} from './GenericFunctions'; + +export class LemlistTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Lemlist Trigger', + name: 'lemlistTrigger', + icon: 'file:lemlist.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle Lemlist events via webhooks', + defaults: { + name: 'Lemlist Trigger', + color: '#4d19ff', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'lemlistApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Email Bounced', + value: 'emailsBounced', + }, + { + name: 'Email Clicked', + value: 'emailsClicked', + }, + { + name: 'Email Opened', + value: 'emailsOpened', + }, + { + name: 'Email Replied', + value: 'emailsReplied', + }, + { + name: 'Email Send Failed', + value: 'emailsSendFailed', + }, + { + name: 'Email Sent', + value: 'emailsSent', + }, + { + name: 'Email Unsubscribed', + value: 'emailsUnsubscribed', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Campaing ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: ` We'll call this hook only for this campaignId.`, + }, + { + displayName: 'Is First', + name: 'isFirst', + type: 'boolean', + default: false, + description: `We'll call this hook only the first time this activity happened.`, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getCampaigns(this: ILoadOptionsFunctions) { + const campaigns = await lemlistApiRequest.call(this, 'GET', '/campaigns'); + return campaigns.map(({ _id, name }: { _id: string, name: string }) => ({ + name, + value: _id, + })); + }, + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhooks = await lemlistApiRequest.call(this, 'GET', '/hooks'); + for (const webhook of webhooks) { + if (webhook.targetUrl === webhookUrl) { + await lemlistApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + return false; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const options = this.getNodeParameter('options') as IDataObject; + const event = this.getNodeParameter('event') as string[]; + const body: IDataObject = { + targetUrl: webhookUrl, + event, + }; + Object.assign(body, options); + const webhook = await lemlistApiRequest.call(this, 'POST', '/hooks', body); + webhookData.webhookId = webhook._id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + try { + await lemlistApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + } catch (error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts new file mode 100644 index 0000000000..c55086c024 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/ActivityDescription.ts @@ -0,0 +1,140 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const activityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'activity', + ], + }, + }, + }, +] as INodeProperties[]; + +export const activityFields = [ + // ---------------------------------- + // activity: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to retrieve activity for.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'emailsOpened', + description: 'Type of activity to retrieve.', + options: [ + { + name: 'Emails Bounced', + value: 'emailsBounced', + }, + { + name: 'Emails Clicked', + value: 'emailsClicked', + }, + { + name: 'Emails Opened', + value: 'emailsOpened', + }, + { + name: 'Emails Replied', + value: 'emailsReplied', + }, + { + name: 'Emails Send Failed', + value: 'emailsSendFailed', + }, + { + name: 'Emails Sent', + value: 'emailsSent', + }, + { + name: 'Emails Unsubscribed', + value: 'emailsUnsubscribed', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts new file mode 100644 index 0000000000..f1702efdcc --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/CampaignDescription.ts @@ -0,0 +1,73 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const campaignOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'campaign', + ], + }, + }, + }, +] as INodeProperties[]; + +export const campaignFields = [ + // ---------------------------------- + // campaign: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'campaign', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts new file mode 100644 index 0000000000..9c4fe039e8 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/LeadDescription.ts @@ -0,0 +1,234 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const leadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'create', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + }, + ], + displayOptions: { + show: { + resource: [ + 'lead', + ], + }, + }, + }, +] as INodeProperties[]; + +export const leadFields = [ + // ---------------------------------- + // lead: create + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to create the lead under.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to create.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + description: 'Company name of the lead to create.', + }, + { + displayName: 'Deduplicate', + name: 'deduplicate', + type: 'boolean', + default: false, + description: 'Do not insert if this email is already present in another campaign.', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the lead to create.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the lead to create.', + }, + ], + }, + + // ---------------------------------- + // lead: delete + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to remove the lead from.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to delete.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // lead: get + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to retrieve.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // lead: unsubscribe + // ---------------------------------- + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign to unsubscribe the lead from.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'unsubscribe', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email of the lead to unsubscribe.', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'unsubscribe', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts new file mode 100644 index 0000000000..c09f51a582 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/TeamDescription.ts @@ -0,0 +1,33 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const teamOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'team', + ], + }, + }, + }, +] as INodeProperties[]; + +export const teamFields = [ + // ---------------------------------- + // team: get + // ---------------------------------- + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts b/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts new file mode 100644 index 0000000000..d35ab0e9ce --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/UnsubscribeDescription.ts @@ -0,0 +1,123 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const unsubscribeOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'add', + description: 'Operation to perform', + options: [ + { + name: 'Add', + value: 'add', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + }, + }, + }, +] as INodeProperties[]; + +export const unsubscribeFields = [ + // ---------------------------------- + // unsubscribe: add + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email to add to the unsubscribes.', + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'add', + ], + }, + }, + }, + + // ---------------------------------- + // unsubscribe: delete + // ---------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email to delete from the unsubscribes.', + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // unsubscribe: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'unsubscribe', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Lemlist/descriptions/index.ts b/packages/nodes-base/nodes/Lemlist/descriptions/index.ts new file mode 100644 index 0000000000..81c322f129 --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/descriptions/index.ts @@ -0,0 +1,5 @@ +export * from './ActivityDescription'; +export * from './CampaignDescription'; +export * from './LeadDescription'; +export * from './TeamDescription'; +export * from './UnsubscribeDescription'; diff --git a/packages/nodes-base/nodes/Lemlist/lemlist.svg b/packages/nodes-base/nodes/Lemlist/lemlist.svg new file mode 100644 index 0000000000..667de83eae --- /dev/null +++ b/packages/nodes-base/nodes/Lemlist/lemlist.svg @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts index 447ce89548..8426bae101 100644 --- a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -56,7 +56,7 @@ export class Mailchimp implements INodeType { description: INodeTypeDescription = { displayName: 'Mailchimp', name: 'mailchimp', - icon: 'file:mailchimp.png', + icon: 'file:mailchimp.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', diff --git a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts index 5a9e039c10..b0f25a882a 100644 --- a/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/MailchimpTrigger.node.ts @@ -19,7 +19,7 @@ export class MailchimpTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Mailchimp Trigger', name: 'mailchimpTrigger', - icon: 'file:mailchimp.png', + icon: 'file:mailchimp.svg', group: ['trigger'], version: 1, description: 'Handle Mailchimp events via webhooks', diff --git a/packages/nodes-base/nodes/Mailchimp/mailchimp.png b/packages/nodes-base/nodes/Mailchimp/mailchimp.png deleted file mode 100644 index cf751b34db..0000000000 Binary files a/packages/nodes-base/nodes/Mailchimp/mailchimp.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Mailchimp/mailchimp.svg b/packages/nodes-base/nodes/Mailchimp/mailchimp.svg new file mode 100644 index 0000000000..9b26e7d3d8 --- /dev/null +++ b/packages/nodes-base/nodes/Mailchimp/mailchimp.svg @@ -0,0 +1,19 @@ + + + mailchimp_node_icon + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts index 8e5c1de699..39eeffb8dd 100644 --- a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts +++ b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; import { IDataObject, @@ -105,80 +105,89 @@ export class Mailgun implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - const fromEmail = this.getNodeParameter('fromEmail') as string; - const toEmail = this.getNodeParameter('toEmail') as string; - const ccEmail = this.getNodeParameter('ccEmail') as string; - const bccEmail = this.getNodeParameter('bccEmail') as string; - const subject = this.getNodeParameter('subject') as string; - const text = this.getNodeParameter('text') as string; - const html = this.getNodeParameter('html') as string; - const attachmentPropertyString = this.getNodeParameter('attachments') as string; + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const credentials = this.getCredentials('mailgunApi'); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + item = items[itemIndex]; - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; + const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; + const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; + const bccEmail = this.getNodeParameter('bccEmail', itemIndex) as string; + const subject = this.getNodeParameter('subject', itemIndex) as string; + const text = this.getNodeParameter('text', itemIndex) as string; + const html = this.getNodeParameter('html', itemIndex) as string; + const attachmentPropertyString = this.getNodeParameter('attachments', itemIndex) as string; - const formData: IDataObject = { - from: fromEmail, - to: toEmail, - subject, - text, - html, - }; + const credentials = this.getCredentials('mailgunApi'); - if (ccEmail.length !== 0) { - formData.cc = ccEmail; - } - if (bccEmail.length !== 0) { - formData.bcc = bccEmail; - } + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } - if (attachmentPropertyString && item.binary) { + const formData: IDataObject = { + from: fromEmail, + to: toEmail, + subject, + text, + html, + }; - const attachments = []; - const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { - return propertyName.trim(); - }); + if (ccEmail.length !== 0) { + formData.cc = ccEmail; + } + if (bccEmail.length !== 0) { + formData.bcc = bccEmail; + } - for (const propertyName of attachmentProperties) { - if (!item.binary.hasOwnProperty(propertyName)) { - continue; - } - attachments.push({ - value: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), - options: { - filename: item.binary[propertyName].fileName || 'unknown', + if (attachmentPropertyString && item.binary) { - }, + const attachments = []; + const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { + return propertyName.trim(); }); + + for (const propertyName of attachmentProperties) { + if (!item.binary.hasOwnProperty(propertyName)) { + continue; + } + attachments.push({ + value: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), + options: { + filename: item.binary[propertyName].fileName || 'unknown', + + }, + }); + } + + if (attachments.length) { + // @ts-ignore + formData.attachment = attachments; + } } - if (attachments.length) { - // @ts-ignore - formData.attachment = attachments; - } + const options = { + method: 'POST', + formData, + uri: `https://${credentials.apiDomain}/v3/${credentials.emailDomain}/messages`, + auth: { + user: 'api', + pass: credentials.apiKey as string, + }, + json: true, + }; + + const responseData = await this.helpers.request(options); + + returnData.push({ + json: responseData, + }); } - - const options = { - method: 'POST', - formData, - uri: `https://${credentials.apiDomain}/v3/${credentials.emailDomain}/messages`, - auth: { - user: 'api', - pass: credentials.apiKey as string, - }, - json: true, - }; - - const responseData = await this.helpers.request(options); - - return { - json: responseData, - }; + return this.prepareOutputData(returnData); } } diff --git a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts index e4618f5919..5d6476a83b 100644 --- a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts +++ b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts @@ -25,7 +25,7 @@ export class Mattermost implements INodeType { description: INodeTypeDescription = { displayName: 'Mattermost', name: 'mattermost', - icon: 'file:mattermost.png', + icon: 'file:mattermost.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -56,6 +56,10 @@ export class Mattermost implements INodeType { name: 'Message', value: 'message', }, + { + name: 'Reaction', + value: 'reaction', + }, { name: 'User', value: 'user', @@ -138,10 +142,46 @@ export class Mattermost implements INodeType { value: 'post', description: 'Post a message into a channel', }, + { + name: 'Post Ephemeral', + value: 'postEphemeral', + description: 'Post an ephemeral message into a channel', + }, ], default: 'post', description: 'The operation to perform', }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'reaction', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a reaction to a post.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Remove a reaction from a post', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all the reactions to one or more posts', + }, + ], + default: 'create', + description: 'The operation to perform', + }, @@ -910,6 +950,73 @@ export class Mattermost implements INodeType { }, ], }, + + // ---------------------------------- + // message:post (ephemeral) + // ---------------------------------- + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'postEphemeral', + ], + resource: [ + 'message', + ], + }, + }, + description: 'ID of the user to send the ephemeral message to.', + }, + { + displayName: 'Channel ID', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'postEphemeral', + ], + resource: [ + 'message', + ], + }, + }, + description: 'ID of the channel to send the ephemeral message in.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + operation: [ + 'postEphemeral', + ], + resource: [ + 'message', + ], + }, + }, + description: 'Text to send in the ephemeral message.', + }, { displayName: 'Other Options', name: 'otherOptions', @@ -937,6 +1044,188 @@ export class Mattermost implements INodeType { }, ], }, + + // ---------------------------------- + // reaction + // ---------------------------------- + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'create', + ], + }, + }, + description: 'ID of the user sending the reaction.', + }, + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + default: '', + placeholder: '3moacfqxmbdw38r38fjprh6zsr', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'create', + ], + }, + }, + description: 'ID of the post to react to.
Obtainable from the post link:
https://mattermost.internal.n8n.io/[server]/pl/[postId]', + }, + { + displayName: 'Emoji Name', + name: 'emojiName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Emoji to use for this reaction.', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'ID of the user whose reaction to delete.', + }, + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + default: '', + placeholder: '3moacfqxmbdw38r38fjprh6zsr', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'ID of the post whose reaction to delete.
Obtainable from the post link:
https://mattermost.internal.n8n.io/[server]/pl/[postId]', + }, + { + displayName: 'Emoji Name', + name: 'emojiName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Name of the emoji to delete.', + }, + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'One or more (comma-separated) posts to retrieve reactions from.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'reaction', + ], + }, + }, + default: true, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'reaction', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + // ---------------------------------- // user // ---------------------------------- @@ -1830,7 +2119,79 @@ export class Mattermost implements INodeType { // Add all the other options to the request const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject; Object.assign(body, otherOptions); + + } else if (operation === 'postEphemeral') { + + // ---------------------------------- + // message:post (ephemeral) + // ---------------------------------- + + // https://api.mattermost.com/#tag/posts/paths/~1posts~1ephemeral/post + + body = { + user_id: this.getNodeParameter('userId', i), + post: { + channel_id: this.getNodeParameter('channelId', i), + message: this.getNodeParameter('message', i), + }, + } as IDataObject; + + requestMethod = 'POST'; + endpoint = 'posts/ephemeral'; + } + + } else if (resource === 'reaction') { + + // ---------------------------------- + // reaction:create + // ---------------------------------- + + // https://api.mattermost.com/#tag/reactions/paths/~1reactions/post + + if (operation === 'create') { + + body = { + user_id: this.getNodeParameter('userId', i), + post_id: this.getNodeParameter('postId', i), + emoji_name: (this.getNodeParameter('emojiName', i) as string).replace(/:/g, ''), + create_at: Date.now(), + } as { user_id: string; post_id: string; emoji_name: string; create_at: number }; + + requestMethod = 'POST'; + endpoint = 'reactions'; + + } else if (operation === 'delete') { + + // ---------------------------------- + // reaction:delete + // ---------------------------------- + + // https://api.mattermost.com/#tag/reactions/paths/~1users~1{user_id}~1posts~1{post_id}~1reactions~1{emoji_name}/delete + + const userId = this.getNodeParameter('userId', i) as string; + const postId = this.getNodeParameter('postId', i) as string; + const emojiName = (this.getNodeParameter('emojiName', i) as string).replace(/:/g, ''); + + requestMethod = 'DELETE'; + endpoint = `users/${userId}/posts/${postId}/reactions/${emojiName}`; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // reaction:getAll + // ---------------------------------- + + // https://api.mattermost.com/#tag/reactions/paths/~1posts~1ids~1reactions/post + + const postId = this.getNodeParameter('postId', i) as string; + + requestMethod = 'GET'; + endpoint = `posts/${postId}/reactions`; + + qs.limit = this.getNodeParameter('limit', 0, 0) as number; + } + } else if (resource === 'user') { if (operation === 'create') { @@ -2006,6 +2367,9 @@ export class Mattermost implements INodeType { responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, body, qs); } else { responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + if (qs.limit) { + responseData = responseData.slice(0, qs.limit); + } if (resource === 'channel' && operation === 'members') { const resolveData = this.getNodeParameter('resolveData', i) as boolean; if (resolveData) { diff --git a/packages/nodes-base/nodes/Mattermost/mattermost.png b/packages/nodes-base/nodes/Mattermost/mattermost.png deleted file mode 100644 index 9a2fe2a49c..0000000000 Binary files a/packages/nodes-base/nodes/Mattermost/mattermost.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Mattermost/mattermost.svg b/packages/nodes-base/nodes/Mattermost/mattermost.svg new file mode 100644 index 0000000000..76d5d58a41 --- /dev/null +++ b/packages/nodes-base/nodes/Mattermost/mattermost.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts b/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts index 3d82fa9920..ab1b642135 100644 --- a/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts +++ b/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts @@ -17,7 +17,7 @@ export class MessageBird implements INodeType { description: INodeTypeDescription = { displayName: 'MessageBird', name: 'messageBird', - icon: 'file:messagebird.png', + icon: 'file:messagebird.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', diff --git a/packages/nodes-base/nodes/MessageBird/messagebird.png b/packages/nodes-base/nodes/MessageBird/messagebird.png deleted file mode 100644 index 006b762950..0000000000 Binary files a/packages/nodes-base/nodes/MessageBird/messagebird.png and /dev/null differ diff --git a/packages/nodes-base/nodes/MessageBird/messagebird.svg b/packages/nodes-base/nodes/MessageBird/messagebird.svg new file mode 100644 index 0000000000..76a221909c --- /dev/null +++ b/packages/nodes-base/nodes/MessageBird/messagebird.svg @@ -0,0 +1,11 @@ + + + messagebird_node_icon + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index 4efade884d..9953484b03 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -36,7 +36,7 @@ export class MicrosoftExcel implements INodeType { description: INodeTypeDescription = { displayName: 'Microsoft Excel', name: 'microsoftExcel', - icon: 'file:excel.png', + icon: 'file:excel.svg', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', diff --git a/packages/nodes-base/nodes/Microsoft/Excel/excel.png b/packages/nodes-base/nodes/Microsoft/Excel/excel.png deleted file mode 100644 index ba2b10c063..0000000000 Binary files a/packages/nodes-base/nodes/Microsoft/Excel/excel.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Microsoft/Excel/excel.svg b/packages/nodes-base/nodes/Microsoft/Excel/excel.svg new file mode 100644 index 0000000000..cb90800486 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/excel.svg @@ -0,0 +1,34 @@ + + + microsoft_excel_node_icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 39516f94f1..16fdcce1dd 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -1,4 +1,7 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; + import { IDataObject, INodeExecutionData, @@ -6,11 +9,16 @@ import { INodeTypeDescription, } from 'n8n-workflow'; -import { chunk, flatten } from '../../utils/utilities'; +import { + chunk, + flatten, +} from '../../utils/utilities'; import * as mssql from 'mssql'; -import { ITables } from './TableInterface'; +import { + ITables, +} from './TableInterface'; import { copyInputItem, @@ -26,13 +34,13 @@ export class MicrosoftSql implements INodeType { description: INodeTypeDescription = { displayName: 'Microsoft SQL', name: 'microsoftSql', - icon: 'file:mssql.png', + icon: 'file:mssql.svg', group: ['input'], version: 1, description: 'Gets, add and update data in Microsoft SQL.', defaults: { name: 'Microsoft SQL', - color: '#1d4bab', + color: '#bcbcbd', }, inputs: ['main'], outputs: ['main'], @@ -217,6 +225,7 @@ export class MicrosoftSql implements INodeType { user: credentials.user as string, password: credentials.password as string, domain: credentials.domain ? (credentials.domain as string) : undefined, + connectTimeout: credentials.connectTimeout as number, options: { encrypt: credentials.tls as boolean, }, diff --git a/packages/nodes-base/nodes/Microsoft/Sql/mssql.png b/packages/nodes-base/nodes/Microsoft/Sql/mssql.png deleted file mode 100644 index 18349dc1cc..0000000000 Binary files a/packages/nodes-base/nodes/Microsoft/Sql/mssql.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Microsoft/Sql/mssql.svg b/packages/nodes-base/nodes/Microsoft/Sql/mssql.svg new file mode 100644 index 0000000000..7fb7859c8a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Sql/mssql.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Microsoft/Teams/ChannelMessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Teams/ChannelMessageDescription.ts index 61879542ad..ede9af39d4 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/ChannelMessageDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/ChannelMessageDescription.ts @@ -129,6 +129,32 @@ export const channelMessageFields = [ default: '', description: 'The content of the item.', }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channelMessage', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Make Reply', + name: 'makeReply', + type: 'string', + default: '', + description: 'An optional ID of the message you want to reply to.', + }, + ], + }, /* -------------------------------------------------------------------------- */ /* channelMessage:getAll */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts index 04d106c750..c6dfb2665a 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts @@ -262,18 +262,27 @@ export class MicrosoftTeams implements INodeType { } if (resource === 'channelMessage') { //https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-beta&tabs=http + //https://docs.microsoft.com/en-us/graph/api/channel-post-messagereply?view=graph-rest-beta&tabs=http if (operation === 'create') { const teamId = this.getNodeParameter('teamId', i) as string; const channelId = this.getNodeParameter('channelId', i) as string; const messageType = this.getNodeParameter('messageType', i) as string; const message = this.getNodeParameter('message', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = { body: { contentType: messageType, content: message, }, }; - responseData = await microsoftApiRequest.call(this, 'POST', `/beta/teams/${teamId}/channels/${channelId}/messages`, body); + + if (options.makeReply) { + const replyToId = options.makeReply as string; + responseData = await microsoftApiRequest.call(this, 'POST', `/beta/teams/${teamId}/channels/${channelId}/messages/${replyToId}/replies`, body); + } else { + responseData = await microsoftApiRequest.call(this, 'POST', `/beta/teams/${teamId}/channels/${channelId}/messages`, body); + } } //https://docs.microsoft.com/en-us/graph/api/channel-list-messages?view=graph-rest-beta&tabs=http if (operation === 'getAll') { diff --git a/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts b/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts index 6bccd6dc38..7f17409585 100644 --- a/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts +++ b/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts @@ -55,6 +55,11 @@ export const boardItemOperations = [ value: 'getByColumnValue', description: 'Get items by column value', }, + { + name: 'Move', + value: 'move', + description: 'Move item to group', + }, ], default: 'create', description: 'The operation to perform.', @@ -63,9 +68,9 @@ export const boardItemOperations = [ export const boardItemFields = [ -/* -------------------------------------------------------------------------- */ -/* boardItem:addUpdate */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:addUpdate */ + /* -------------------------------------------------------------------------- */ { displayName: 'Item ID', name: 'itemId', @@ -102,9 +107,9 @@ export const boardItemFields = [ }, description: 'The update text to add.', }, -/* -------------------------------------------------------------------------- */ -/* boardItem:changeColumnValue */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:changeColumnValue */ + /* -------------------------------------------------------------------------- */ { displayName: 'Board ID', name: 'boardId', @@ -186,9 +191,9 @@ export const boardItemFields = [ }, description: 'The column value in JSON format. Documentation can be found here.', }, -/* -------------------------------------------------------------------------- */ -/* boardItem:changeMultipleColumnValues */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:changeMultipleColumnValues */ + /* -------------------------------------------------------------------------- */ { displayName: 'Board ID', name: 'boardId', @@ -249,9 +254,9 @@ export const boardItemFields = [ alwaysOpenEditWindow: true, }, }, -/* -------------------------------------------------------------------------- */ -/* boardItem:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Board ID', name: 'boardId', @@ -342,9 +347,9 @@ export const boardItemFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* boardItem:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Item ID', name: 'itemId', @@ -363,9 +368,9 @@ export const boardItemFields = [ }, description: `Item's ID`, }, -/* -------------------------------------------------------------------------- */ -/* boardItem:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Item ID', name: 'itemId', @@ -384,9 +389,9 @@ export const boardItemFields = [ }, description: `Item's ID (Multiple can be added separated by comma)`, }, -/* -------------------------------------------------------------------------- */ -/* boardItem:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Board ID', name: 'boardId', @@ -471,9 +476,9 @@ export const boardItemFields = [ default: 50, description: 'How many results to return.', }, -/* -------------------------------------------------------------------------- */ -/* boardItem:getByColumnValue */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* boardItem:getByColumnValue */ + /* -------------------------------------------------------------------------- */ { displayName: 'Board ID', name: 'boardId', @@ -578,4 +583,68 @@ export const boardItemFields = [ default: 50, description: 'How many results to return.', }, + /* -------------------------------------------------------------------------- */ + /* boardItem:move */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Board ID', + name: 'boardId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBoards', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'move', + ], + }, + }, + }, + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'move', + ], + resource: [ + 'boardItem', + ], + }, + }, + default: '', + description: `The item's ID`, + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getGroups', + loadOptionsDependsOn: [ + 'boardId', + ], + }, + required: true, + displayOptions: { + show: { + resource: [ + 'boardItem', + ], + operation: [ + 'move', + ], + }, + }, + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts index b790df1b6c..f1aa20f977 100644 --- a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts +++ b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts @@ -38,7 +38,7 @@ import { import { snakeCase, - } from 'change-case'; +} from 'change-case'; interface IGraphqlBody { query: string; @@ -49,7 +49,7 @@ export class MondayCom implements INodeType { description: INodeTypeDescription = { displayName: 'Monday.com', name: 'mondayCom', - icon: 'file:mondayCom.png', + icon: 'file:mondayCom.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -322,7 +322,7 @@ export class MondayCom implements INodeType { if (returnAll === true) { responseData = await mondayComApiRequestAllItems.call(this, 'data.boards', body); } else { - body.variables.limit = this.getNodeParameter('limit', i) as number; + body.variables.limit = this.getNodeParameter('limit', i) as number; responseData = await mondayComApiRequest.call(this, body); responseData = responseData.data.boards; } @@ -695,6 +695,26 @@ export class MondayCom implements INodeType { responseData = responseData.data.items_by_column_values; } } + if (operation === 'move') { + const groupId = this.getNodeParameter('groupId', i) as string; + const itemId = parseInt(this.getNodeParameter('itemId', i) as string, 10); + + const body: IGraphqlBody = { + query: + `mutation ($groupId: String!, $itemId: Int!) { + move_item_to_group (group_id: $groupId, item_id: $itemId) { + id + } + }`, + variables: { + groupId, + itemId, + }, + }; + + responseData = await mondayComApiRequest.call(this, body); + responseData = responseData.data.move_item_to_group; + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); diff --git a/packages/nodes-base/nodes/MondayCom/mondayCom.png b/packages/nodes-base/nodes/MondayCom/mondayCom.png deleted file mode 100644 index 83a0a2c004..0000000000 Binary files a/packages/nodes-base/nodes/MondayCom/mondayCom.png and /dev/null differ diff --git a/packages/nodes-base/nodes/MondayCom/mondayCom.svg b/packages/nodes-base/nodes/MondayCom/mondayCom.svg new file mode 100644 index 0000000000..5abd596adc --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/mondayCom.svg @@ -0,0 +1,11 @@ + + + monday_node_icon + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index e9abdc2bd2..c03a62816f 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -1,14 +1,26 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; + import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow'; -import { nodeDescription } from './mongo.node.options'; -import { MongoClient } from 'mongodb'; + +import { + nodeDescription, +} from './mongo.node.options'; + +import { + MongoClient, + ObjectID, +} from 'mongodb'; + import { getItemCopy, + handleDateFields, validateAndResolveMongoCredentials } from './mongo.node.utils'; @@ -65,8 +77,13 @@ export class MongoDb implements INodeType { .map(f => f.trim()) .filter(f => !!f); + const options = this.getNodeParameter('options', 0) as IDataObject; const insertItems = getItemCopy(items, fields); + if (options.dateFields) { + handleDateFields(insertItems, options.dateFields as string); + } + const { insertedIds } = await mdb .collection(this.getNodeParameter('collection', 0) as string) .insertMany(insertItems); @@ -90,6 +107,8 @@ export class MongoDb implements INodeType { .map(f => f.trim()) .filter(f => !!f); + const options = this.getNodeParameter('options', 0) as IDataObject; + let updateKey = this.getNodeParameter('updateKey', 0) as string; updateKey = updateKey.trim(); @@ -100,19 +119,25 @@ export class MongoDb implements INodeType { // Prepare the data to update and copy it to be returned const updateItems = getItemCopy(items, fields); + if (options.dateFields) { + handleDateFields(updateItems, options.dateFields as string); + } + for (const item of updateItems) { if (item[updateKey] === undefined) { continue; } - const filter: { [key: string]: string } = {}; + const filter: { [key: string]: string | ObjectID } = {}; filter[updateKey] = item[updateKey] as string; - + if (updateKey === '_id') { + filter[updateKey] = new ObjectID(filter[updateKey]); + delete item['_id']; + } await mdb .collection(this.getNodeParameter('collection', 0) as string) .updateOne(filter, { $set: item }); } - returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]); } else { throw new Error(`The operation "${operation}" is not supported!`); diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts index d4bd0a37b5..5fa8972c51 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -1,4 +1,6 @@ -import { INodeTypeDescription } from 'n8n-workflow'; +import { + INodeTypeDescription, +} from 'n8n-workflow'; /** * Options to be displayed @@ -6,7 +8,7 @@ import { INodeTypeDescription } from 'n8n-workflow'; export const nodeDescription: INodeTypeDescription = { displayName: 'MongoDB', name: 'mongoDb', - icon: 'file:mongoDb.png', + icon: 'file:mongodb.svg', group: ['input'], version: 1, description: 'Find, insert and update documents in MongoDB.', @@ -46,7 +48,7 @@ export const nodeDescription: INodeTypeDescription = { { name: 'Update', value: 'update', - description: 'Updates documents.', + description: 'Update documents.', }, ], default: 'find', @@ -97,7 +99,9 @@ export const nodeDescription: INodeTypeDescription = { }, displayOptions: { show: { - operation: ['find'], + operation: [ + 'find', + ], }, }, default: '{}', @@ -115,7 +119,9 @@ export const nodeDescription: INodeTypeDescription = { type: 'string', displayOptions: { show: { - operation: ['insert'], + operation: [ + 'insert', + ], }, }, default: '', @@ -133,7 +139,9 @@ export const nodeDescription: INodeTypeDescription = { type: 'string', displayOptions: { show: { - operation: ['update'], + operation: [ + 'update', + ], }, }, default: 'id', @@ -147,7 +155,9 @@ export const nodeDescription: INodeTypeDescription = { type: 'string', displayOptions: { show: { - operation: ['update'], + operation: [ + 'update', + ], }, }, default: '', @@ -155,5 +165,29 @@ export const nodeDescription: INodeTypeDescription = { description: 'Comma separated list of the fields to be included into the new document.', }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'update', + 'insert', + ], + }, + }, + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Date Fields', + name: 'dateFields', + type: 'string', + default: '', + description: 'Comma separeted list of fields that will be parse as Mongo Date type.', + }, + ], + }, ], }; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts index bc2d308a76..87fefd69b3 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -102,3 +102,14 @@ export function getItemCopy( return newItem; }); } + +export function handleDateFields(insertItems: IDataObject[], fields: string) { + const dateFields = (fields as string).split(','); + for (let i = 0; i < insertItems.length; i++) { + for (const key of Object.keys(insertItems[i])) { + if (dateFields.includes(key)) { + insertItems[i][key] = new Date(insertItems[i][key] as string); + } + } + } +} diff --git a/packages/nodes-base/nodes/MongoDb/mongoDb.png b/packages/nodes-base/nodes/MongoDb/mongoDb.png deleted file mode 100644 index 0980afcb2b..0000000000 Binary files a/packages/nodes-base/nodes/MongoDb/mongoDb.png and /dev/null differ diff --git a/packages/nodes-base/nodes/MongoDb/mongodb.svg b/packages/nodes-base/nodes/MongoDb/mongodb.svg new file mode 100644 index 0000000000..80d3a99ac2 --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongodb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/MoveBinaryData.node.ts b/packages/nodes-base/nodes/MoveBinaryData.node.ts index b715bf3971..666f8c3790 100644 --- a/packages/nodes-base/nodes/MoveBinaryData.node.ts +++ b/packages/nodes-base/nodes/MoveBinaryData.node.ts @@ -4,15 +4,41 @@ import { unset, } from 'lodash'; +import { + BINARY_ENCODING, +} from 'n8n-core'; + import { IExecuteFunctions } from 'n8n-core'; import { IBinaryData, IDataObject, INodeExecutionData, + INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; +import * as iconv from 'iconv-lite'; +iconv.encodingExists('utf8'); + +// Create options for bomAware and encoding +const bomAware: string[] = []; +const encodeDecodeOptions: INodePropertyOptions[] = []; +const encodings = (iconv as any).encodings; // tslint:disable-line:no-any +Object.keys(encodings).forEach(encoding => { + if (!(encoding.startsWith('_') || typeof encodings[encoding] === 'string')) { // only encodings without direct alias or internals + if (encodings[encoding].bomAware) { + bomAware.push(encoding); + } + encodeDecodeOptions.push({ name: encoding, value: encoding }); + } +}); + +encodeDecodeOptions.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; +}); export class MoveBinaryData implements INodeType { description: INodeTypeDescription = { @@ -189,17 +215,47 @@ export class MoveBinaryData implements INodeType { { displayName: 'Encoding', name: 'encoding', - type: 'string', + type: 'options', + options: encodeDecodeOptions, displayOptions: { show: { '/mode': [ 'binaryToJson', + 'jsonToBinary', ], }, }, default: 'utf8', description: 'Set the encoding of the data stream', }, + { + displayName: 'Strip BOM', + name: 'stripBOM', + displayOptions: { + show: { + '/mode': [ + 'binaryToJson', + ], + encoding: bomAware, + }, + }, + type: 'boolean', + default: true, + }, + { + displayName: 'Add BOM', + name: 'addBOM', + displayOptions: { + show: { + '/mode': [ + 'jsonToBinary', + ], + encoding: bomAware, + }, + }, + type: 'boolean', + default: false, + }, { displayName: 'File Name', name: 'fileName', @@ -336,19 +392,19 @@ export class MoveBinaryData implements INodeType { continue; } - const encoding = (options.encoding as BufferEncoding) || 'utf8'; + const encoding = (options.encoding as string) || 'utf8'; let convertedValue = value.data; if (setAllData === true) { // Set the full data - convertedValue = Buffer.from(convertedValue, 'base64').toString(encoding); + convertedValue = iconv.decode(Buffer.from(convertedValue, BINARY_ENCODING), encoding, { stripBOM: options.stripBOM as boolean }); newItem.json = JSON.parse(convertedValue); } else { // Does get added to existing data so copy it first newItem.json = JSON.parse(JSON.stringify(item.json)); if (options.keepAsBase64 !== true) { - convertedValue = Buffer.from(convertedValue, 'base64').toString(encoding); + convertedValue = iconv.decode(Buffer.from(convertedValue, BINARY_ENCODING), encoding, { stripBOM: options.stripBOM as boolean }); } if (options.jsonParse) { @@ -372,6 +428,7 @@ export class MoveBinaryData implements INodeType { const convertAllData = this.getNodeParameter('convertAllData', itemIndex) as boolean; const destinationKey = this.getNodeParameter('destinationKey', itemIndex) as string; + const encoding = (options.encoding as string) || 'utf8'; let value: IDataObject | string = item.json; if (convertAllData === false) { const sourceKey = this.getNodeParameter('sourceKey', itemIndex) as string; @@ -396,7 +453,7 @@ export class MoveBinaryData implements INodeType { value = JSON.stringify(value); } - value = Buffer.from(value as string).toString('base64'); + value = iconv.encode(value as string, encoding, { addBOM: options.addBOM as boolean }).toString(BINARY_ENCODING); } const convertedValue: IBinaryData = { diff --git a/packages/nodes-base/nodes/MySql/MySql.node.ts b/packages/nodes-base/nodes/MySql/MySql.node.ts index 43974e766d..5015954d98 100644 --- a/packages/nodes-base/nodes/MySql/MySql.node.ts +++ b/packages/nodes-base/nodes/MySql/MySql.node.ts @@ -190,9 +190,8 @@ export class MySql implements INodeType { return connection.query(rawQuery); }); - let queryResult = await Promise.all(queryQueue); - queryResult = queryResult.reduce((collection, result) => { + const queryResult = (await Promise.all(queryQueue) as mysql2.OkPacket[][]).reduce((collection, result) => { const [rows, fields] = result; if (Array.isArray(rows)) { @@ -204,7 +203,7 @@ export class MySql implements INodeType { return collection; }, []); - returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); + returnItems = this.helpers.returnJsonArray(queryResult as unknown as IDataObject[]); } else if (operation === 'insert') { // ---------------------------------- @@ -220,7 +219,7 @@ export class MySql implements INodeType { const queryItems = insertItems.reduce((collection, item) => collection.concat(Object.values(item as any)), []); // tslint:disable-line:no-any const queryResult = await connection.query(insertSQL, queryItems); - returnItems = this.helpers.returnJsonArray(queryResult[0] as IDataObject); + returnItems = this.helpers.returnJsonArray(queryResult[0] as unknown as IDataObject); } else if (operation === 'update') { // ---------------------------------- @@ -239,10 +238,8 @@ export class MySql implements INodeType { const updateItems = copyInputItems(items, columns); const updateSQL = `UPDATE ${table} SET ${columns.map(column => `${column} = ?`).join(',')} WHERE ${updateKey} = ?;`; const queryQueue = updateItems.map((item) => connection.query(updateSQL, Object.values(item).concat(item[updateKey]))); - let queryResult = await Promise.all(queryQueue); - - queryResult = queryResult.map(result => result[0]); - returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); + const queryResult = await Promise.all(queryQueue); + returnItems = this.helpers.returnJsonArray(queryResult.map(result => result[0]) as unknown as IDataObject[]); } else { await connection.end(); diff --git a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts index 096b39427f..dd65e4d86b 100644 --- a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts +++ b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts @@ -233,7 +233,7 @@ export class NextCloud implements INodeType { }, }, placeholder: '/invoices/original.txt', - description: 'The path of file or folder to copy.', + description: 'The path of file or folder to copy. The path should start with "/"', }, { displayName: 'To Path', @@ -253,7 +253,7 @@ export class NextCloud implements INodeType { }, }, placeholder: '/invoices/copy.txt', - description: 'The destination path of file or folder.', + description: 'The destination path of file or folder. The path should start with "/"', }, // ---------------------------------- @@ -276,8 +276,8 @@ export class NextCloud implements INodeType { ], }, }, - placeholder: 'invoices/2019/invoice_1.pdf', - description: 'The path to delete. Can be a single file or a whole folder.', + placeholder: '/invoices/2019/invoice_1.pdf', + description: 'The path to delete. Can be a single file or a whole folder. The path should start with "/"', }, // ---------------------------------- @@ -301,7 +301,7 @@ export class NextCloud implements INodeType { }, }, placeholder: '/invoices/old_name.txt', - description: 'The path of file or folder to move.', + description: 'The path of file or folder to move. The path should start with "/"', }, { displayName: 'To Path', @@ -321,7 +321,7 @@ export class NextCloud implements INodeType { }, }, placeholder: '/invoices/new_name.txt', - description: 'The new path of file or folder.', + description: 'The new path of file or folder. The path should start with "/"', }, // ---------------------------------- @@ -343,8 +343,8 @@ export class NextCloud implements INodeType { ], }, }, - placeholder: 'invoices/2019/invoice_1.pdf', - description: 'The file path of the file to download. Has to contain the full path.', + placeholder: '/invoices/2019/invoice_1.pdf', + description: 'The file path of the file to download. Has to contain the full path. The path should start with "/"', }, { displayName: 'Binary Property', @@ -384,8 +384,8 @@ export class NextCloud implements INodeType { ], }, }, - placeholder: 'invoices/2019/invoice_1.pdf', - description: 'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', + placeholder: '/invoices/2019/invoice_1.pdf', + description: 'The absolute file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', }, { displayName: 'Binary Data', @@ -476,8 +476,8 @@ export class NextCloud implements INodeType { ], }, }, - placeholder: 'invoices/2019', - description: 'The folder to create. The parent folder has to exist.', + placeholder: '/invoices/2019', + description: 'The folder to create. The parent folder has to exist. The path should start with "/"', }, // ---------------------------------- @@ -498,8 +498,8 @@ export class NextCloud implements INodeType { ], }, }, - placeholder: 'invoices/2019/', - description: 'The path of which to list the content.', + placeholder: '/invoices/2019/', + description: 'The path of which to list the content. The path should start with "/"', }, // ---------------------------------- diff --git a/packages/nodes-base/nodes/Oura/GenericFunctions.ts b/packages/nodes-base/nodes/Oura/GenericFunctions.ts new file mode 100644 index 0000000000..cd52b4bb4d --- /dev/null +++ b/packages/nodes-base/nodes/Oura/GenericFunctions.ts @@ -0,0 +1,63 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function ouraApiRequest( + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + option: IDataObject = {}, +) { + + const credentials = this.getCredentials('ouraApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + method, + qs, + body, + uri: uri || `https://api.ouraring.com/v1${resource}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + options = Object.assign({}, options, option); + + try { + return await this.helpers.request!(options); + } catch (error) { + + const errorMessage = error?.response?.body?.message; + + if (errorMessage) { + throw new Error(`Oura error response [${error.statusCode}]: ${errorMessage}`); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Oura/Oura.node.ts b/packages/nodes-base/nodes/Oura/Oura.node.ts new file mode 100644 index 0000000000..58dfeb8890 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/Oura.node.ts @@ -0,0 +1,178 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + ouraApiRequest, +} from './GenericFunctions'; + +import { + profileOperations, +} from './ProfileDescription'; + +import { + summaryFields, + summaryOperations, +} from './SummaryDescription'; + +import * as moment from 'moment'; + +export class Oura implements INodeType { + description: INodeTypeDescription = { + displayName: 'Oura', + name: 'oura', + icon: 'file:oura.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Oura API', + defaults: { + name: 'Oura', + color: '#2f4a73', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'ouraApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Profile', + value: 'profile', + }, + { + name: 'Summary', + value: 'summary', + }, + ], + default: 'summary', + description: 'Resource to consume.', + }, + ...profileOperations, + ...summaryOperations, + ...summaryFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = items.length; + + let responseData; + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + + if (resource === 'profile') { + + // ********************************************************************* + // profile + // ********************************************************************* + + // https://cloud.ouraring.com/docs/personal-info + + if (operation === 'get') { + + // ---------------------------------- + // profile: get + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/userinfo'); + + } + + } else if (resource === 'summary') { + + // ********************************************************************* + // summary + // ********************************************************************* + + // https://cloud.ouraring.com/docs/daily-summaries + + const qs: IDataObject = {}; + + const { start, end } = this.getNodeParameter('filters', i) as { start: string; end: string; }; + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (start) { + qs.start = moment(start).format('YYYY-MM-DD'); + } + + if (end) { + qs.end = moment(end).format('YYYY-MM-DD'); + } + + if (operation === 'getActivity') { + + // ---------------------------------- + // profile: getActivity + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/activity', {}, qs); + responseData = responseData.activity; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } else if (operation === 'getReadiness') { + + // ---------------------------------- + // profile: getReadiness + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/readiness', {}, qs); + responseData = responseData.readiness; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } else if (operation === 'getSleep') { + + // ---------------------------------- + // profile: getSleep + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/sleep', {}, qs); + responseData = responseData.sleep; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } + + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Oura/ProfileDescription.ts b/packages/nodes-base/nodes/Oura/ProfileDescription.ts new file mode 100644 index 0000000000..b3e028c8a8 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/ProfileDescription.ts @@ -0,0 +1,27 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const profileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'profile', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get the user\'s personal information.', + }, + ], + default: 'get', + description: 'Operation to perform.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Oura/SummaryDescription.ts b/packages/nodes-base/nodes/Oura/SummaryDescription.ts new file mode 100644 index 0000000000..0512eb1e22 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/SummaryDescription.ts @@ -0,0 +1,105 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const summaryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'summary', + ], + }, + }, + options: [ + { + name: 'Get Activity Summary', + value: 'getActivity', + description: 'Get the user\'s activity summary.', + }, + { + name: 'Get Readiness Summary', + value: 'getReadiness', + description: 'Get the user\'s readiness summary.', + }, + { + name: 'Get Sleep Periods', + value: 'getSleep', + description: 'Get the user\'s sleep summary.', + }, + ], + default: 'getSleep', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const summaryFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'summary', + ], + }, + }, + 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: [ + 'summary', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'summary', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'End Date', + name: 'end', + type: 'dateTime', + default: '', + description: 'End date for the summary retrieval. If omitted, it defaults to the current day.', + }, + { + displayName: 'Start Date', + name: 'start', + type: 'dateTime', + default: '', + description: 'Start date for the summary retrieval. If omitted, it defaults to a week ago.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Oura/oura.svg b/packages/nodes-base/nodes/Oura/oura.svg new file mode 100644 index 0000000000..4d4a1adc66 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/oura.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/packages/nodes-base/nodes/PayPal/PayPal.node.ts b/packages/nodes-base/nodes/PayPal/PayPal.node.ts index 0fbc56d80b..f22e1c93f3 100644 --- a/packages/nodes-base/nodes/PayPal/PayPal.node.ts +++ b/packages/nodes-base/nodes/PayPal/PayPal.node.ts @@ -31,7 +31,7 @@ export class PayPal implements INodeType { description: INodeTypeDescription = { displayName: 'PayPal', name: 'payPal', - icon: 'file:paypal.png', + icon: 'file:paypal.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', diff --git a/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts b/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts index 2962d92600..94b69ade48 100644 --- a/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts +++ b/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts @@ -20,7 +20,7 @@ export class PayPalTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'PayPal Trigger', name: 'payPalTrigger', - icon: 'file:paypal.png', + icon: 'file:paypal.svg', group: ['trigger'], version: 1, description: 'Handle PayPal events via webhooks', diff --git a/packages/nodes-base/nodes/PayPal/paypal.png b/packages/nodes-base/nodes/PayPal/paypal.png deleted file mode 100644 index 6780c5de67..0000000000 Binary files a/packages/nodes-base/nodes/PayPal/paypal.png and /dev/null differ diff --git a/packages/nodes-base/nodes/PayPal/paypal.svg b/packages/nodes-base/nodes/PayPal/paypal.svg new file mode 100644 index 0000000000..2d457931fc --- /dev/null +++ b/packages/nodes-base/nodes/PayPal/paypal.svg @@ -0,0 +1,12 @@ + + + paypal_node_icon + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Peekalink/Peekalink.node.json b/packages/nodes-base/nodes/Peekalink/Peekalink.node.json new file mode 100644 index 0000000000..00f4b0836a --- /dev/null +++ b/packages/nodes-base/nodes/Peekalink/Peekalink.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.peekalink", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/peekalink" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.peekalink/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Plivo/CallDescription.ts b/packages/nodes-base/nodes/Plivo/CallDescription.ts new file mode 100644 index 0000000000..7db6e2e19f --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/CallDescription.ts @@ -0,0 +1,117 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const callOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'call', + ], + }, + }, + options: [ + { + name: 'Make', + value: 'make', + description: 'Make a voice call', + }, + ], + default: 'make', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const callFields = [ + // ---------------------------------- + // call: make + // ---------------------------------- + { + displayName: 'From', + name: 'from', + type: 'string', + default: '', + placeholder: '+14156667777', + description: 'Caller ID for the call to make.', + required: true, + displayOptions: { + show: { + resource: [ + 'call', + ], + operation: [ + 'make', + ], + }, + }, + }, + { + displayName: 'To', + name: 'to', + type: 'string', + default: '', + placeholder: '+14156667778', + required: true, + description: 'Phone number to make the call to.', + displayOptions: { + show: { + resource: [ + 'call', + ], + operation: [ + 'make', + ], + }, + }, + }, + { + displayName: 'Answer Method', + name: 'answer_method', + type: 'options', + required: true, + description: 'HTTP verb to be used when invoking the Answer URL.', + default: 'POST', + options: [ + { + name: 'GET', + value: 'GET', + }, + { + name: 'POST', + value: 'POST', + }, + ], + displayOptions: { + show: { + resource: [ + 'call', + ], + operation: [ + 'make', + ], + }, + }, + }, + { + displayName: 'Answer URL', + name: 'answer_url', + type: 'string', + default: '', + description: 'URL to be invoked by Plivo once the call is answered.
It should return the XML to handle the call once answered.', + required: true, + displayOptions: { + show: { + resource: [ + 'call', + ], + operation: [ + 'make', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Plivo/GenericFunctions.ts b/packages/nodes-base/nodes/Plivo/GenericFunctions.ts new file mode 100644 index 0000000000..6af3a48acd --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/GenericFunctions.ts @@ -0,0 +1,62 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to Plivo. + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function plivoApiRequest( + this: IHookFunctions | IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + + const credentials = this.getCredentials('plivoApi') as { authId: string, authToken: string }; + + if (!credentials) { + throw new Error('No credentials returned!'); + } + + const options = { + method, + form: body, + qs, + uri: `https://api.plivo.com/v1/Account/${credentials.authId}${endpoint}/`, + auth: { + user: credentials.authId, + pass: credentials.authToken, + }, + json: true, + }; + + try { + return await this.helpers.request(options); + } catch (error) { + if (error.statusCode === 401) { + throw new Error('Invalid Plivo credentials'); + } + if (error?.response?.body?.error) { + let errorMessage = `Plivo error response [${error.statusCode}]: ${error.response.body.error}`; + if (error.response.body.more_info) { + errorMessage = `errorMessage (${error.response.body.more_info})`; + } + + throw new Error(errorMessage); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Plivo/MmsDescription.ts b/packages/nodes-base/nodes/Plivo/MmsDescription.ts new file mode 100644 index 0000000000..398ae5f182 --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/MmsDescription.ts @@ -0,0 +1,107 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const mmsOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'mms', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send an MMS message (US/Canada only)', + }, + ], + default: 'send', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const mmsFields = [ + // ---------------------------------- + // mms: send + // ---------------------------------- + { + displayName: 'From', + name: 'from', + type: 'string', + default: '', + description: 'Plivo Number to send the MMS from.', + placeholder: '+14156667777', + required: true, + displayOptions: { + show: { + resource: [ + 'mms', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'To', + name: 'to', + type: 'string', + default: '', + description: 'Phone number to send the MMS to.', + placeholder: '+14156667778', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'mms', + ], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: 'Message to send.', + required: false, + displayOptions: { + show: { + resource: [ + 'mms', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Media URLs', + name: 'media_urls', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + resource: [ + 'mms', + ], + operation: [ + 'send', + ], + }, + }, + description: 'Comma-separated list of media URLs of the files from your file server.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Plivo/Plivo.node.json b/packages/nodes-base/nodes/Plivo/Plivo.node.json new file mode 100644 index 0000000000..64e49874aa --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/Plivo.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.plivo", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication", + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/plivo" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.plivo/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Plivo/Plivo.node.ts b/packages/nodes-base/nodes/Plivo/Plivo.node.ts new file mode 100644 index 0000000000..3a7623ea14 --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/Plivo.node.ts @@ -0,0 +1,178 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + smsFields, + smsOperations, +} from './SmsDescription'; + +import { + mmsFields, + mmsOperations, +} from './MmsDescription'; + +import { + callFields, + callOperations, +} from './CallDescription'; + +import { + plivoApiRequest, +} from './GenericFunctions'; + +export class Plivo implements INodeType { + description: INodeTypeDescription = { + displayName: 'Plivo', + name: 'plivo', + icon: 'file:plivo.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Send SMS/MMS messages or make phone calls', + defaults: { + name: 'Plivo', + color: '#43A046', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'plivoApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Call', + value: 'call', + }, + { + name: 'MMS', + value: 'mms', + }, + { + name: 'SMS', + value: 'sms', + }, + ], + default: 'sms', + required: true, + description: 'The resource to operate on.', + }, + ...smsOperations, + ...smsFields, + ...mmsOperations, + ...mmsFields, + ...callOperations, + ...callFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < items.length; i++) { + + let responseData; + + if (resource === 'sms') { + + // ********************************************************************* + // sms + // ********************************************************************* + + if (operation === 'send') { + + // ---------------------------------- + // sms: send + // ---------------------------------- + + const body = { + src: this.getNodeParameter('from', i) as string, + dst: this.getNodeParameter('to', i) as string, + text: this.getNodeParameter('message', i) as string, + } as IDataObject; + + responseData = await plivoApiRequest.call(this, 'POST', '/Message', body); + + } + + } else if (resource === 'call') { + + // ********************************************************************* + // call + // ********************************************************************* + + if (operation === 'make') { + + // ---------------------------------- + // call: make + // ---------------------------------- + + // https://www.plivo.com/docs/voice/api/call#make-a-call + + const body = { + from: this.getNodeParameter('from', i) as string, + to: this.getNodeParameter('to', i) as string, + answer_url: this.getNodeParameter('answer_url', i) as string, + answer_method: this.getNodeParameter('answer_method', i) as string, + } as IDataObject; + + responseData = await plivoApiRequest.call(this, 'POST', '/Call', body); + + } + + } else if (resource === 'mms') { + + // ********************************************************************* + // mms + // ********************************************************************* + + if (operation === 'send') { + + // ---------------------------------- + // mss: send + // ---------------------------------- + + // https://www.plivo.com/docs/sms/api/message#send-a-message + + const body = { + src: this.getNodeParameter('from', i) as string, + dst: this.getNodeParameter('to', i) as string, + text: this.getNodeParameter('message', i) as string, + type: 'mms', + media_urls: this.getNodeParameter('media_urls', i) as string, + } as IDataObject; + + responseData = await plivoApiRequest.call(this, 'POST', '/Message', body); + + } + + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Plivo/SmsDescription.ts b/packages/nodes-base/nodes/Plivo/SmsDescription.ts new file mode 100644 index 0000000000..ef1774fb93 --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/SmsDescription.ts @@ -0,0 +1,89 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const smsOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sms', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send an SMS message.', + }, + ], + default: 'send', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const smsFields = [ + // ---------------------------------- + // sms: send + // ---------------------------------- + { + displayName: 'From', + name: 'from', + type: 'string', + default: '', + description: 'Plivo Number to send the SMS from.', + placeholder: '+14156667777', + required: true, + displayOptions: { + show: { + resource: [ + 'sms', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'To', + name: 'to', + type: 'string', + default: '', + description: 'Phone number to send the message to.', + placeholder: '+14156667778', + required: true, + displayOptions: { + show: { + resource: [ + 'sms', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + description: 'Message to send.', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Plivo/plivo.svg b/packages/nodes-base/nodes/Plivo/plivo.svg new file mode 100644 index 0000000000..445ddb2563 --- /dev/null +++ b/packages/nodes-base/nodes/Plivo/plivo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/PostHog/AliasDescription.ts b/packages/nodes-base/nodes/PostHog/AliasDescription.ts new file mode 100644 index 0000000000..072a450dd7 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/AliasDescription.ts @@ -0,0 +1,126 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const aliasOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'alias', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an alias', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const aliasFields = [ + + /* -------------------------------------------------------------------------- */ + /* alias:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Alias', + name: 'alias', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'alias', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The name of the alias.', + }, + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'alias', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The user's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'alias', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Context', + name: 'contextUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Context', + name: 'contextValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/EventDescription.ts b/packages/nodes-base/nodes/PostHog/EventDescription.ts new file mode 100644 index 0000000000..920f636c87 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/EventDescription.ts @@ -0,0 +1,126 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an event', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + + /* -------------------------------------------------------------------------- */ + /* event:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event', + name: 'eventName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The name of the event.', + }, + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The user's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'propertyValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/GenericFunctions.ts b/packages/nodes-base/nodes/PostHog/GenericFunctions.ts new file mode 100644 index 0000000000..ad64cc006d --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/GenericFunctions.ts @@ -0,0 +1,80 @@ +import { + OptionsWithUrl, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function posthogApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('postHogApi') as IDataObject; + + const base = credentials.url as string; + + body.api_key = credentials.apiKey as string; + + const options: OptionsWithUrl = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + url: `${base}${path}`, + json: true, + }; + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + return await this.helpers.request!(options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + + const message = error.response.body.message; + // Try to return the error prettier + throw new Error( + `PosHog error response [${error.statusCode}]: ${message}`, + ); + } + throw error; + } +} + +export interface IEvent { + event: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any +} + +export interface IAlias { + type: string; + event: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any + context: { [key: string]: any }; // tslint:disable-line:no-any +} + +export interface ITrack { + type: string; + event: string; + name: string; + messageId?: string; + distinct_id: string; + category?: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any + context: { [key: string]: any }; // tslint:disable-line:no-any +} + + +export interface IIdentity { + event: string; + messageId?: string; + distinct_id: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any +} diff --git a/packages/nodes-base/nodes/PostHog/IdentityDescription.ts b/packages/nodes-base/nodes/PostHog/IdentityDescription.ts new file mode 100644 index 0000000000..4f9aff6138 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/IdentityDescription.ts @@ -0,0 +1,113 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const identityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'identity', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const identityFields = [ + + /* -------------------------------------------------------------------------- */ + /* identity:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'identity', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The identity's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'identity', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'propertyValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/PostHog.node.json b/packages/nodes-base/nodes/PostHog/PostHog.node.json new file mode 100644 index 0000000000..c2dde1fcb0 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/PostHog.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.postHog", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Analytics", + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/postHog" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.postHog/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/PostHog/PostHog.node.ts b/packages/nodes-base/nodes/PostHog/PostHog.node.ts new file mode 100644 index 0000000000..f56860731a --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/PostHog.node.ts @@ -0,0 +1,248 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + IAlias, + IEvent, + IIdentity, + ITrack, + posthogApiRequest, +} from './GenericFunctions'; + +import { + aliasFields, + aliasOperations, +} from './AliasDescription'; + +import { + eventFields, + eventOperations, +} from './EventDescription'; + +import { + trackFields, + trackOperations, +} from './TrackDescription'; + +import { + identityFields, + identityOperations, +} from './IdentityDescription'; + +import * as moment from 'moment-timezone'; + +export class PostHog implements INodeType { + description: INodeTypeDescription = { + displayName: 'PostHog', + name: 'postHog', + icon: 'file:postHog.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume PostHog API.', + defaults: { + name: 'PostHog', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'postHogApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Alias', + value: 'alias', + }, + { + name: 'Event', + value: 'event', + }, + { + name: 'Identity', + value: 'identity', + }, + { + name: 'Track', + value: 'track', + }, + ], + default: 'event', + description: 'The resource to operate on.', + }, + ...aliasOperations, + ...aliasFields, + ...eventOperations, + ...eventFields, + ...identityOperations, + ...identityFields, + ...trackOperations, + ...trackFields, + ], + }; + + 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; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'alias') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const alias = this.getNodeParameter('alias', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const context = (additionalFields.contextUi as IDataObject || {}).contextValues as IDataObject[] || []; + + const event: IAlias = { + type: 'alias', + event: '$create_alias', + context: context.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + properties: { + distinct_id: distinctId, + alias, + }, + }; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + + responseData = await posthogApiRequest.call(this, 'POST', '/batch', event); + + returnData.push(responseData); + } + } + } + + if (resource === 'event') { + if (operation === 'create') { + const events: IEvent[] = []; + for (let i = 0; i < length; i++) { + const eventName = this.getNodeParameter('eventName', i) as string; + + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const properties = (additionalFields.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + + const event: IEvent = { + event: eventName, + properties: properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + }; + + event.properties['distinct_id'] = distinctId; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + //@ts-ignore + delete event.propertiesUi; + + events.push(event); + } + + responseData = await posthogApiRequest.call(this, 'POST', '/capture', { batch: events }); + + returnData.push(responseData); + } + } + + if (resource === 'identity') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const properties = (additionalFields.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + + const event: IIdentity = { + event: '$identify', + properties: properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + distinct_id: distinctId, + }; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + //@ts-ignore + delete event.propertiesUi; + + responseData = await posthogApiRequest.call(this, 'POST', '/batch', event); + + returnData.push(responseData); + } + } + } + + if (resource === 'track') { + if (operation === 'page' || operation === 'screen') { + for (let i = 0; i < length; i++) { + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const context = (additionalFields.contextUi as IDataObject || {}).contextValues as IDataObject[] || []; + + const properties = (additionalFields.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + + const event: ITrack = { + name, + type: operation, + event: `$${operation}`, + context: context.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + distinct_id: distinctId, + properties: properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + }; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + //@ts-ignore + delete event.propertiesUi; + + responseData = await posthogApiRequest.call(this, 'POST', '/batch', event); + + returnData.push(responseData); + } + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/PostHog/TrackDescription.ts b/packages/nodes-base/nodes/PostHog/TrackDescription.ts new file mode 100644 index 0000000000..fb45d329f6 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/TrackDescription.ts @@ -0,0 +1,175 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const trackOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'track', + ], + }, + }, + options: [ + { + name: 'Page', + value: 'page', + description: 'Track a page', + }, + { + name: 'Screen', + value: 'screen', + description: 'Track a screen', + }, + ], + default: 'page', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const trackFields = [ + + /* -------------------------------------------------------------------------- */ + /* track:page */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'page', + 'screen', + ], + }, + }, + default: '', + }, + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'page', + 'screen', + ], + }, + }, + default: '', + description: `The user's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'page', + 'screen', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Category', + name: 'category', + type: 'string', + default: '', + }, + { + displayName: 'Context', + name: 'contextUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Context', + name: 'contextValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + }, + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'propertyValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/postHog.svg b/packages/nodes-base/nodes/PostHog/postHog.svg new file mode 100644 index 0000000000..e62492556c --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/postHog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts index eaeebe313e..df1f10cbbc 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts @@ -68,19 +68,22 @@ export async function pgInsert( const schema = getNodeParam('schema', 0) as string; let returnFields = (getNodeParam('returnFields', 0) as string).split(',') as string[]; const columnString = getNodeParam('columns', 0) as string; - const columns = columnString.split(',').map(column => column.trim()); - - const cs = new pgp.helpers.ColumnSet(columns); + const columns = columnString.split(',') + .map(column => column.trim().split(':')) + .map(([name, cast]) => ({ name, cast })); const te = new pgp.helpers.TableName({ table, schema }); // Prepare the data to insert and copy it to be returned - const insertItems = getItemCopy(items, columns); + const columnNames = columns.map(column => column.name); + const insertItems = getItemCopy(items, columnNames); + + const columnSet = new pgp.helpers.ColumnSet(columns); // Generate the multi-row insert query and return the id of new row returnFields = returnFields.map(value => value.trim()).filter(value => !!value); const query = - pgp.helpers.insert(insertItems, cs, te) + + pgp.helpers.insert(insertItems, columnSet, te) + (returnFields.length ? ` RETURNING ${returnFields.join(',')}` : ''); // Executing the query to insert the data @@ -109,21 +112,36 @@ export async function pgUpdate( const updateKey = getNodeParam('updateKey', 0) as string; const columnString = getNodeParam('columns', 0) as string; - const columns = columnString.split(',').map(column => column.trim()); + const [updateColumnName, updateColumnCast] = updateKey.split(':'); + const updateColumn = { + name: updateColumnName, + cast: updateColumnCast, + }; + + const columns = columnString.split(',') + .map(column => column.trim().split(':')) + .map(([name, cast]) => ({ name, cast })); const te = new pgp.helpers.TableName({ table, schema }); // Make sure that the updateKey does also get queried - if (!columns.includes(updateKey)) { - columns.unshift(updateKey); + const targetCol = columns.find((column) => column.name === updateColumn.name); + if (!targetCol) { + columns.unshift(updateColumn); + } + else if (!targetCol.cast) { + targetCol.cast = updateColumn.cast || targetCol.cast; } // Prepare the data to update and copy it to be returned - const updateItems = getItemCopy(items, columns); + const columnNames = columns.map(column => column.name); + const updateItems = getItemCopy(items, columnNames); + + const columnSet = new pgp.helpers.ColumnSet(columns); // Generate the multi-row update query const query = - pgp.helpers.update(updateItems, columns, te) + ' WHERE v.' + updateKey + ' = t.' + updateKey; + pgp.helpers.update(updateItems, columnSet, te) + ' WHERE v.' + updateColumn.name + ' = t.' + updateColumn.name; // Executing the query to update the data await db.none(query); diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index 1628510841..1250236d78 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -116,9 +116,9 @@ export class Postgres implements INodeType { }, }, default: '', - placeholder: 'id,name,description', + placeholder: 'id:int,name:text,description', description: - 'Comma separated list of the properties which should used as columns for the new rows.', + 'Comma separated list of the properties which should used as columns for the new rows.
You can use type casting with colons (:) like id:int.', }, { displayName: 'Return Fields', @@ -186,9 +186,9 @@ export class Postgres implements INodeType { }, }, default: '', - placeholder: 'name,description', + placeholder: 'name:text,description', description: - 'Comma separated list of the properties which should used as columns for rows to update.', + 'Comma separated list of the properties which should used as columns for rows to update.
You can use type casting with colons (:) like id:int.', }, ], }; diff --git a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts index 15e7bcb2a5..c4b9036f0e 100644 --- a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts +++ b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts @@ -107,22 +107,6 @@ export class QuestDb implements INodeType { required: true, description: 'Name of the table in which to insert data to.', }, - { - displayName: 'Columns', - name: 'columns', - type: 'string', - displayOptions: { - show: { - operation: [ - 'insert', - ], - }, - }, - default: '', - placeholder: 'id,name,description', - description: - 'Comma separated list of the properties which should used as columns for the new rows.', - }, { displayName: 'Return Fields', name: 'returnFields', @@ -194,7 +178,7 @@ export class QuestDb implements INodeType { }).join(','); const query = `INSERT INTO ${tableName} (${columns.join(',')}) VALUES (${values});`; - queries.push(query); + queries.push(query); }); await db.any(pgp.helpers.concat(queries)); diff --git a/packages/nodes-base/nodes/QuestDb/questdb.png b/packages/nodes-base/nodes/QuestDb/questdb.png index 5be1906e5d..3ff95d14a7 100644 Binary files a/packages/nodes-base/nodes/QuestDb/questdb.png and b/packages/nodes-base/nodes/QuestDb/questdb.png differ diff --git a/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts b/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts new file mode 100644 index 0000000000..9218f82bad --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts @@ -0,0 +1,436 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + CustomField, + GeneralAddress, + Ref, +} from './descriptions/Shared.interface'; + +import { + capitalCase, +} from 'change-case'; + +import { + pickBy, +} from 'lodash'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to QuickBooks. + */ +export async function quickBooksApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject, + option: IDataObject = {}, +): Promise { // tslint:disable-line:no-any + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let isDownload = false; + + if (['estimate', 'invoice', 'payment'].includes(resource) && operation === 'get') { + isDownload = this.getNodeParameter('download', 0) as boolean; + } + + const productionUrl = 'https://quickbooks.api.intuit.com'; + const sandboxUrl = 'https://sandbox-quickbooks.api.intuit.com'; + + const credentials = this.getCredentials('quickBooksOAuth2Api') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + }, + method, + uri: `${credentials.environment === 'sandbox' ? sandboxUrl : productionUrl}${endpoint}`, + qs, + body, + json: !isDownload, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + if (isDownload) { + options.headers!['Accept'] = 'application/pdf'; + } + + if (resource === 'invoice' && operation === 'send') { + options.headers!['Content-Type'] = 'application/octet-stream'; + } + + if ( + (resource === 'invoice' && (operation === 'void' || operation === 'delete')) || + (resource === 'payment' && (operation === 'void' || operation === 'delete')) + ) { + options.headers!['Content-Type'] = 'application/json'; + } + + try { + return await this.helpers.requestOAuth2!.call(this, 'quickBooksOAuth2Api', options); + } catch (error) { + + const errors = error.error.Fault.Error; + + if (errors && Array.isArray(errors)) { + const errorMessage = errors.map( + (e) => `QuickBooks error response [${e.code}]: ${e.Message} - Detail: ${e.Detail}`, + ).join('|'); + + throw new Error(errorMessage); + } + + throw error; + } +} + +/** + * Make an authenticated API request to QuickBooks and return all results. + */ +export async function quickBooksApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject, + resource: string, +): Promise { // tslint:disable-line:no-any + + let responseData; + let startPosition = 1; + const maxResults = 1000; + const returnData: IDataObject[] = []; + + const maxCount = await getCount.call(this, method, endpoint, qs); + + const originalQuery = qs.query; + + do { + qs.query = `${originalQuery} MAXRESULTS ${maxResults} STARTPOSITION ${startPosition}`; + responseData = await quickBooksApiRequest.call(this, method, endpoint, qs, body); + returnData.push(...responseData.QueryResponse[capitalCase(resource)]); + startPosition += maxResults; + + } while (maxCount > returnData.length); + + return returnData; +} + +async function getCount( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, +): Promise { // tslint:disable-line:no-any + + const responseData = await quickBooksApiRequest.call(this, method, endpoint, qs, {}); + + return responseData.QueryResponse.totalCount; +} + +/** + * Handles a QuickBooks listing by returning all items or up to a limit. + */ +export async function handleListing( + this: IExecuteFunctions, + i: number, + endpoint: string, + resource: string, +): Promise { // tslint:disable-line:no-any + let responseData; + + const qs = { + query: `SELECT * FROM ${resource}`, + } as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i); + + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.query) { + qs.query += ` ${filters.query}`; + } + + if (returnAll) { + return await quickBooksApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.query += ` MAXRESULTS ${limit}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, qs, {}); + responseData = responseData.QueryResponse[capitalCase(resource)]; + return responseData; + } +} + +/** + * Get the SyncToken required for delete and void operations in QuickBooks. + */ +export async function getSyncToken( + this: IExecuteFunctions, + i: number, + companyId: string, + resource: string, +) { + const resourceId = this.getNodeParameter(`${resource}Id`, i); + const getEndpoint = `/v3/company/${companyId}/${resource}/${resourceId}`; + const propertyName = capitalCase(resource); + const { [propertyName]: { SyncToken } } = await quickBooksApiRequest.call(this, 'GET', getEndpoint, {}, {}); + + return SyncToken; +} + +/** + * Get the reference and SyncToken required for update operations in QuickBooks. + */ +export async function getRefAndSyncToken( + this: IExecuteFunctions, + i: number, + companyId: string, + resource: string, + ref: string, +) { + const resourceId = this.getNodeParameter(`${resource}Id`, i); + const endpoint = `/v3/company/${companyId}/${resource}/${resourceId}`; + const responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + + return { + ref: responseData[capitalCase(resource)][ref], + syncToken: responseData[capitalCase(resource)].SyncToken, + }; + +} + +/** + * Populate node items with binary data. + */ +export async function handleBinaryData( + this: IExecuteFunctions, + items: INodeExecutionData[], + i: number, + companyId: string, + resource: string, + resourceId: string, +) { + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + const fileName = this.getNodeParameter('fileName', i) as string; + const endpoint = `/v3/company/${companyId}/${resource}/${resourceId}/pdf`; + const data = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}, { encoding: null }); + + items[i].binary = items[i].binary ?? {}; + items[i].binary![binaryProperty] = await this.helpers.prepareBinaryData(data); + items[i].binary![binaryProperty].fileName = fileName; + items[i].binary![binaryProperty].fileExtension = 'pdf'; + + return items; +} + +export async function loadResource( + this: ILoadOptionsFunctions, + resource: string, +) { + const returnData: INodePropertyOptions[] = []; + + const qs = { + query: `SELECT * FROM ${resource}`, + } as IDataObject; + + const { oauthTokenData: { callbackQueryString: { realmId } } } = this.getCredentials('quickBooksOAuth2Api') as { oauthTokenData: { callbackQueryString: { realmId: string } } }; + const endpoint = `/v3/company/${realmId}/query`; + + const resourceItems = await quickBooksApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + + if (resource === 'preferences') { + const { SalesFormsPrefs: { CustomField } } = resourceItems[0]; + const customFields = CustomField[1].CustomField; + for (const customField of customFields) { + const length = customField.Name.length; + returnData.push({ + name: customField.StringValue, + value: customField.Name.charAt(length - 1), + }); + } + return returnData; + } + + resourceItems.forEach((resourceItem: { DisplayName: string, Name: string, Id: string }) => { + returnData.push({ + name: resourceItem.DisplayName || resourceItem.Name, + value: resourceItem.Id, + }); + }); + + return returnData; +} + +/** + * Populate the `Line` property in a request body. + */ +export function processLines( + this: IExecuteFunctions, + body: IDataObject, + lines: IDataObject[], + resource: string, +) { + + lines.forEach((line) => { + if (resource === 'bill') { + + if (line.DetailType === 'AccountBasedExpenseLineDetail') { + line.AccountBasedExpenseLineDetail = { + AccountRef: { + value: line.accountId, + }, + }; + delete line.accountId; + } else if (line.DetailType === 'ItemBasedExpenseLineDetail') { + line.ItemBasedExpenseLineDetail = { + ItemRef: { + value: line.itemId, + }, + }; + delete line.itemId; + } + + } else if (resource === 'estimate') { + if (line.DetailType === 'SalesItemLineDetail') { + line.SalesItemLineDetail = { + ItemRef: { + value: line.itemId, + }, + }; + delete line.itemId; + } + + } else if (resource === 'invoice') { + if (line.DetailType === 'SalesItemLineDetail') { + line.SalesItemLineDetail = { + ItemRef: { + value: line.itemId, + }, + }; + delete line.itemId; + } + } + + }); + + return lines; +} + +/** + * Populate update fields or additional fields into a request body. + */ +export function populateFields( + this: IExecuteFunctions, + body: IDataObject, + fields: IDataObject, + resource: string, +) { + + Object.entries(fields).forEach(([key, value]) => { + + if (resource === 'bill') { + + if (key.endsWith('Ref')) { + const { details } = value as { details: Ref }; + body[key] = { + name: details.name, + value: details.value, + }; + + } else { + body[key] = value; + } + + } else if (['customer', 'employee', 'vendor'].includes(resource)) { + + if (key === 'BillAddr') { + const { details } = value as { details: GeneralAddress }; + body.BillAddr = pickBy(details, detail => detail !== ''); + + } else if (key === 'PrimaryEmailAddr') { + body.PrimaryEmailAddr = { + Address: value, + }; + + } else if (key === 'PrimaryPhone') { + body.PrimaryPhone = { + FreeFormNumber: value, + }; + + } else { + body[key] = value; + } + + } else if (resource === 'estimate' || resource === 'invoice') { + + if (key === 'BillAddr' || key === 'ShipAddr') { + const { details } = value as { details: GeneralAddress }; + body[key] = pickBy(details, detail => detail !== ''); + + } else if (key === 'BillEmail') { + body.BillEmail = { + Address: value, + }; + + } else if (key === 'CustomFields') { + const { Field } = value as { Field: CustomField[] }; + body.CustomField = Field; + const length = (body.CustomField as CustomField[]).length; + for (let i = 0; i < length; i++) { + //@ts-ignore + body.CustomField[i]['Type'] = 'StringType'; + } + + } else if (key === 'CustomerMemo') { + body.CustomerMemo = { + value, + }; + + } else if (key.endsWith('Ref')) { + const { details } = value as { details: Ref }; + body[key] = { + name: details.name, + value: details.value, + }; + + } else if (key === 'TotalTax') { + body.TxnTaxDetail = { + TotalTax: value, + }; + + } else { + body[key] = value; + } + + } else if (resource === 'payment') { + body[key] = value; + } + }); + return body; +} diff --git a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.json b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.json new file mode 100644 index 0000000000..859beeb312 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.quickbooks", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Finance & Accounting" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/quickbooks" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.quickbooks/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts new file mode 100644 index 0000000000..4fe8f983df --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts @@ -0,0 +1,990 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + billFields, + billOperations, + customerFields, + customerOperations, + employeeFields, + employeeOperations, + estimateFields, + estimateOperations, + invoiceFields, + invoiceOperations, + itemFields, + itemOperations, + paymentFields, + paymentOperations, + vendorFields, + vendorOperations, +} from './descriptions'; + +import { + getRefAndSyncToken, + getSyncToken, + handleBinaryData, + handleListing, + loadResource, + populateFields, + processLines, + quickBooksApiRequest, +} from './GenericFunctions'; + +import { + capitalCase, +} from 'change-case'; + +import { + isEmpty, +} from 'lodash'; + +export class QuickBooks implements INodeType { + description: INodeTypeDescription = { + displayName: 'QuickBooks', + name: 'quickbooks', + icon: 'file:quickbooks.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the QuickBooks API', + defaults: { + name: 'QuickBooks', + color: '#2CA01C', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'quickBooksOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Bill', + value: 'bill', + }, + { + name: 'Customer', + value: 'customer', + }, + { + name: 'Employee', + value: 'employee', + }, + { + name: 'Estimate', + value: 'estimate', + }, + { + name: 'Invoice', + value: 'invoice', + }, + { + name: 'Item', + value: 'item', + }, + { + name: 'Payment', + value: 'payment', + }, + { + name: 'Vendor', + value: 'vendor', + }, + ], + default: 'customer', + description: 'Resource to consume', + }, + ...billOperations, + ...billFields, + ...customerOperations, + ...customerFields, + ...employeeOperations, + ...employeeFields, + ...estimateOperations, + ...estimateFields, + ...invoiceOperations, + ...invoiceFields, + ...itemOperations, + ...itemFields, + ...paymentOperations, + ...paymentFields, + ...vendorOperations, + ...vendorFields, + ], + }; + + methods = { + loadOptions: { + async getCustomers(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'customer'); + }, + + async getCustomFields(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'preferences'); + }, + + async getItems(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'item'); + }, + + async getVendors(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'vendor'); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + const { oauthTokenData } = this.getCredentials('quickBooksOAuth2Api') as IDataObject; + // @ts-ignore + const companyId = oauthTokenData.callbackQueryString.realmId; + + for (let i = 0; i < items.length; i++) { + + if (resource === 'bill') { + + // ********************************************************************* + // bill + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/estimate + + if (operation === 'create') { + + // ---------------------------------- + // bill: create + // ---------------------------------- + + const lines = this.getNodeParameter('Line', i) as IDataObject[]; + + if (!lines.length) { + throw new Error(`Please enter at least one line for the ${resource}.`); + } + + if (lines.some(line => line.DetailType === undefined || line.Amount === undefined || line.Description === undefined)) { + throw new Error('Please enter detail type, amount and description for every line.'); + } + + lines.forEach(line => { + if (line.DetailType === 'AccountBasedExpenseLineDetail' && line.accountId === undefined) { + throw new Error('Please enter an account ID for the associated line.'); + } else if (line.DetailType === 'ItemBasedExpenseLineDetail' && line.itemId === undefined) { + throw new Error('Please enter an item ID for the associated line.'); + } + }); + + let body = { + VendorRef: { + value: this.getNodeParameter('VendorRef', i), + }, + } as IDataObject; + + body.Line = processLines.call(this, body, lines, resource); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // bill: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('billId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // bill: get + // ---------------------------------- + + const billId = this.getNodeParameter('billId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${billId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // bill: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // bill: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'VendorRef'); + + let body = { + Id: this.getNodeParameter('billId', i), + SyncToken: syncToken, + sparse: true, + VendorRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'customer') { + + // ********************************************************************* + // customer + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/customer + + if (operation === 'create') { + + // ---------------------------------- + // customer: create + // ---------------------------------- + + let body = { + DisplayName: this.getNodeParameter('displayName', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // customer: get + // ---------------------------------- + + const customerId = this.getNodeParameter('customerId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${customerId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // customer: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // customer: update + // ---------------------------------- + + let body = { + Id: this.getNodeParameter('customerId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + sparse: true, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'employee') { + + // ********************************************************************* + // employee + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // employee: create + // ---------------------------------- + + let body = { + FamilyName: this.getNodeParameter('FamilyName', i), + GivenName: this.getNodeParameter('GivenName', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // employee: get + // ---------------------------------- + + const employeeId = this.getNodeParameter('employeeId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${employeeId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // employee: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // employee: update + // ---------------------------------- + + let body = { + Id: this.getNodeParameter('employeeId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + sparse: true, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'estimate') { + + // ********************************************************************* + // estimate + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/estimate + + if (operation === 'create') { + + // ---------------------------------- + // estimate: create + // ---------------------------------- + + const lines = this.getNodeParameter('Line', i) as IDataObject[]; + + if (!lines.length) { + throw new Error(`Please enter at least one line for the ${resource}.`); + } + + if (lines.some(line => line.DetailType === undefined || line.Amount === undefined || line.Description === undefined)) { + throw new Error('Please enter detail type, amount and description for every line.'); + } + + lines.forEach(line => { + if (line.DetailType === 'SalesItemLineDetail' && line.itemId === undefined) { + throw new Error('Please enter an item ID for the associated line.'); + } + }); + + let body = { + CustomerRef: { + value: this.getNodeParameter('CustomerRef', i), + }, + } as IDataObject; + + body.Line = processLines.call(this, body, lines, resource); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // estimate: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('estimateId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // estimate: get + // ---------------------------------- + + const estimateId = this.getNodeParameter('estimateId', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + if (download) { + + responseData = await handleBinaryData.call(this, items, i, companyId, resource, estimateId); + + } else { + + const endpoint = `/v3/company/${companyId}/${resource}/${estimateId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // estimate: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'send') { + + // ---------------------------------- + // estimate: send + // ---------------------------------- + + const estimateId = this.getNodeParameter('estimateId', i) as string; + + const qs = { + sendTo: this.getNodeParameter('email', i) as string, + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}/${estimateId}/send`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'update') { + + // ---------------------------------- + // estimate: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'CustomerRef'); + + let body = { + Id: this.getNodeParameter('estimateId', i), + SyncToken: syncToken, + sparse: true, + CustomerRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'invoice') { + + // ********************************************************************* + // invoice + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/invoice + + if (operation === 'create') { + + // ---------------------------------- + // invoice: create + // ---------------------------------- + + const lines = this.getNodeParameter('Line', i) as IDataObject[]; + + if (!lines.length) { + throw new Error(`Please enter at least one line for the ${resource}.`); + } + + if (lines.some(line => line.DetailType === undefined || line.Amount === undefined || line.Description === undefined)) { + throw new Error('Please enter detail type, amount and description for every line.'); + } + + lines.forEach(line => { + if (line.DetailType === 'SalesItemLineDetail' && line.itemId === undefined) { + throw new Error('Please enter an item ID for the associated line.'); + } + }); + + let body = { + CustomerRef: { + value: this.getNodeParameter('CustomerRef', i), + }, + } as IDataObject; + + body.Line = processLines.call(this, body, lines, resource); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // invoice: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('invoiceId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // invoice: get + // ---------------------------------- + + const invoiceId = this.getNodeParameter('invoiceId', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + if (download) { + + responseData = await handleBinaryData.call(this, items, i, companyId, resource, invoiceId); + + } else { + + const endpoint = `/v3/company/${companyId}/${resource}/${invoiceId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // invoice: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'send') { + + // ---------------------------------- + // invoice: send + // ---------------------------------- + + const invoiceId = this.getNodeParameter('invoiceId', i) as string; + + const qs = { + sendTo: this.getNodeParameter('email', i) as string, + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}/${invoiceId}/send`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'update') { + + // ---------------------------------- + // invoice: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'CustomerRef'); + + let body = { + Id: this.getNodeParameter('invoiceId', i), + SyncToken: syncToken, + sparse: true, + CustomerRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'void') { + + // ---------------------------------- + // invoice: void + // ---------------------------------- + + const qs = { + Id: this.getNodeParameter('invoiceId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + operation: 'void', + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'item') { + + // ********************************************************************* + // item + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/item + + if (operation === 'get') { + + // ---------------------------------- + // item: get + // ---------------------------------- + + const item = this.getNodeParameter('itemId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${item}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // item: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } + + } else if (resource === 'payment') { + + // ********************************************************************* + // payment + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/payment + + if (operation === 'create') { + + // ---------------------------------- + // payment: create + // ---------------------------------- + + let body = { + CustomerRef: { + value: this.getNodeParameter('CustomerRef', i), + }, + TotalAmt: this.getNodeParameter('TotalAmt', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // payment: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('paymentId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // payment: get + // ---------------------------------- + + const paymentId = this.getNodeParameter('paymentId', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + if (download) { + + responseData = await handleBinaryData.call(this, items, i, companyId, resource, paymentId); + + } else { + + const endpoint = `/v3/company/${companyId}/${resource}/${paymentId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // payment: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'send') { + + // ---------------------------------- + // payment: send + // ---------------------------------- + + const paymentId = this.getNodeParameter('paymentId', i) as string; + + const qs = { + sendTo: this.getNodeParameter('email', i) as string, + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}/${paymentId}/send`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'update') { + + // ---------------------------------- + // payment: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'CustomerRef'); + + let body = { + Id: this.getNodeParameter('paymentId', i), + SyncToken: syncToken, + sparse: true, + CustomerRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'void') { + + // ---------------------------------- + // payment: void + // ---------------------------------- + + const qs = { + Id: this.getNodeParameter('paymentId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + operation: 'void', + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'vendor') { + + // ********************************************************************* + // vendor + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/vendor + + if (operation === 'create') { + + // ---------------------------------- + // vendor: create + // ---------------------------------- + + let body = { + DisplayName: this.getNodeParameter('displayName', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // vendor: get + // ---------------------------------- + + const vendorId = this.getNodeParameter('vendorId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${vendorId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // vendor: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // vendor: update + // ---------------------------------- + + let body = { + Id: this.getNodeParameter('vendorId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + sparse: true, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + const download = this.getNodeParameter('download', 0, false) as boolean; + + if (['invoice', 'estimate', 'payment'].includes(resource) && ['get'].includes(operation) && download) { + return this.prepareOutputData(responseData); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..5d872f6461 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts @@ -0,0 +1,86 @@ +export const billAdditionalFieldsOptions = [ + { + displayName: 'Accounts Payable Account', + name: 'APAccountRef', + placeholder: 'Add APA Fields', + description: 'Accounts Payable account to which the bill will be credited.', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'ID', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Balance', + name: 'Balance', + description: 'The balance reflecting any payments made against the transaction.', + type: 'string', + default: '', + }, + { + displayName: 'Due Date', + name: 'DueDate', + description: 'Date when the payment of the transaction is due.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Sales Term', + name: 'SalesTermRef', + description: 'Sales term associated with the transaction.', + placeholder: 'Add Sales Term Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'ID', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts new file mode 100644 index 0000000000..9a1dcff0f9 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts @@ -0,0 +1,330 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billAdditionalFieldsOptions, +} from './BillAdditionalFieldsOptions'; + +export const billOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'bill', + ], + }, + }, + }, +] as INodeProperties[]; + +export const billFields = [ + // ---------------------------------- + // bill: create + // ---------------------------------- + { + displayName: 'For Vendor', + name: 'VendorRef', + type: 'options', + required: true, + description: 'The ID of the vendor who the bill is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getVendors', + }, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Line', + name: 'Line', + type: 'collection', + placeholder: 'Add Line Item Property', + description: 'Individual line item of a transaction.', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Detail Type', + name: 'DetailType', + type: 'options', + default: 'ItemBasedExpenseLineDetail', + options: [ + { + name: 'Account-Based Expense Line Detail', + value: 'AccountBasedExpenseLineDetail', + }, + { + name: 'Item-Based Expense Line Detail', + value: 'ItemBasedExpenseLineDetail', + }, + ], + }, + { + displayName: 'Item', + name: 'itemId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getItems', + }, + }, + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + default: '', + }, + { + displayName: 'Amount', + name: 'Amount', + description: 'Monetary amount of the line item.', + type: 'number', + default: 0, + }, + { + displayName: 'Description', + name: 'Description', + description: 'Textual description of the line item.', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Position', + name: 'LineNum', + description: 'Position of the line item relative to others.', + type: 'number', + default: 1, + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'create', + ], + }, + }, + options: billAdditionalFieldsOptions, + }, + + // ---------------------------------- + // bill: delete + // ---------------------------------- + { + displayName: 'Bill ID', + name: 'billId', + type: 'string', + required: true, + default: '', + description: 'The ID of the bill to delete.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // bill: get + // ---------------------------------- + { + displayName: 'Bill ID', + name: 'billId', + type: 'string', + required: true, + default: '', + description: 'The ID of the bill to retrieve.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // bill: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting bills. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // bill: update + // ---------------------------------- + { + displayName: 'Bill ID', + name: 'billId', + type: 'string', + required: true, + default: '', + description: 'The ID of the bill to update.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'update', + ], + }, + }, + // filter out fields that cannot be updated + options: billAdditionalFieldsOptions.filter(property => property.name !== 'TotalAmt' && property.name !== 'Balance'), + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..db037f9ff5 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts @@ -0,0 +1,151 @@ +export const customerAdditionalFieldsOptions = [ + { + displayName: 'Active', + name: 'Active', + description: 'Whether the customer is currently enabled for use by QuickBooks.', + type: 'boolean', + default: true, + }, + { + displayName: 'Balance', + name: 'Balance', + description: 'Open balance amount or amount unpaid by the customer.', + type: 'string', + default: '', + }, + { + displayName: 'Balance With Jobs', + name: 'BalanceWithJobs', + description: 'Cumulative open balance amount for the customer (or job) and all its sub-jobs.', + type: 'number', + default: 0, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Bill With Parent', + name: 'BillWithParent', + description: 'Bill this customer together with its parent.', + type: 'boolean', + default: false, + }, + { + displayName: 'Company Name', + name: 'CompanyName', + type: 'string', + default: '', + }, + { + displayName: 'Family Name', + name: 'FamilyName', + type: 'string', + default: '', + }, + { + displayName: 'Fully Qualified Name', + name: 'FullyQualifiedName', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'GivenName', + type: 'string', + default: '', + }, + { + displayName: 'Preferred Delivery Method', + name: 'PreferredDeliveryMethod', + type: 'options', + default: 'Print', + options: [ + { + name: 'Print', + value: 'Print', + }, + { + name: 'Email', + value: 'Email', + }, + { + name: 'None', + value: 'None', + }, + ], + }, + { + displayName: 'Primary Email Address', + name: 'PrimaryEmailAddr', + type: 'string', + default: '', + }, + { + displayName: 'Primary Phone', + name: 'PrimaryPhone', + type: 'string', + default: '', + }, + { + displayName: 'Print-On-Check Name', + name: 'PrintOnCheckName', + description: 'Name of the customer as printed on a check.', + type: 'string', + default: '', + }, + { + displayName: 'Taxable', + name: 'Taxable', + description: 'Whether transactions for this customer are taxable.', + type: 'boolean', + default: false, + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts new file mode 100644 index 0000000000..f4735d2b55 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts @@ -0,0 +1,222 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + customerAdditionalFieldsOptions, +} from './CustomerAdditionalFieldsOptions'; + +export const customerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'customer', + ], + }, + }, + }, +] as INodeProperties[]; + +export const customerFields = [ + // ---------------------------------- + // customer: create + // ---------------------------------- + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + required: true, + default: '', + description: 'The display name of the customer to create.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', + ], + }, + }, + options: customerAdditionalFieldsOptions, + }, + + // ---------------------------------- + // customer: get + // ---------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + required: true, + default: '', + description: 'The ID of the customer to retrieve.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // customer: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting customers. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // customer: update + // ---------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + required: true, + default: '', + description: 'The ID of the customer to update.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update', + ], + }, + }, + options: customerAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..e10304ad33 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts @@ -0,0 +1,91 @@ +export const employeeAdditionalFieldsOptions = [ + { + displayName: 'Active', + name: 'Active', + description: 'Whether the employee is currently enabled for use by QuickBooks.', + type: 'boolean', + default: false, + }, + { + displayName: 'Billable Time', + name: 'BillableTime', + type: 'boolean', + default: false, + }, + { + displayName: 'Display Name', + name: 'DisplayName', + type: 'string', + default: '', + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Primary Phone', + name: 'PrimaryPhone', + type: 'string', + default: '', + }, + { + displayName: 'Print-On-Check Name', + name: 'PrintOnCheckName', + description: 'Name of the employee as printed on a check.', + type: 'string', + default: '', + }, + { + displayName: 'Social Security Number', + name: 'SSN', + type: 'string', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts new file mode 100644 index 0000000000..7e6b001aed --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts @@ -0,0 +1,236 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + employeeAdditionalFieldsOptions, +} from './EmployeeAdditionalFieldsOptions'; + +export const employeeOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'employee', + ], + }, + }, + }, +] as INodeProperties[]; + +export const employeeFields = [ + // ---------------------------------- + // employee: create + // ---------------------------------- + { + displayName: 'Family Name', + name: 'FamilyName', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Given Name', + name: 'GivenName', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'create', + ], + }, + }, + options: employeeAdditionalFieldsOptions, + }, + + // ---------------------------------- + // employee: get + // ---------------------------------- + { + displayName: 'Employee ID', + name: 'employeeId', + type: 'string', + required: true, + default: '', + description: 'The ID of the employee to retrieve.', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // employee: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting employees. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // employee: update + // ---------------------------------- + { + displayName: 'Employee ID', + name: 'employeeId', + type: 'string', + required: true, + default: '', + description: 'The ID of the employee to update.', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'update', + ], + }, + }, + options: employeeAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..9532b7f442 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts @@ -0,0 +1,227 @@ +export const estimateAdditionalFieldsOptions = [ + { + displayName: 'Apply Tax After Discount', + name: 'ApplyTaxAfterDiscount', + type: 'boolean', + default: false, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Billing Email', + name: 'BillEmail', + description: 'E-mail address to which the estimate will be sent.', + type: 'string', + default: '', + }, + { + displayName: 'Custom Fields', + name: 'CustomFields', + placeholder: 'Add Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'Field', + values: [ + { + displayName: 'Field Definition ID', + name: 'DefinitionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: 'ID of the field to set.', + }, + { + displayName: 'Field Value', + name: 'StringValue', + type: 'string', + default: '', + description: 'Value of the field to set.', + }, + ], + }, + ], + }, + { + displayName: 'Customer Memo', + name: 'CustomerMemo', + description: 'User-entered message to the customer. This message is visible to end user on their transactions.', + type: 'string', + default: '', + }, + { + displayName: 'Document Number', + name: 'DocNumber', + description: 'Reference number for the transaction.', + type: 'string', + default: '', + }, + { + displayName: 'Email Status', + name: 'EmailStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Send', + value: 'NeedToSend', + }, + { + name: 'Email Sent', + value: 'EmailSent', + }, + ], + }, + { + displayName: 'Print Status', + name: 'PrintStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Print', + value: 'NeedToPrint', + }, + { + name: 'PrintComplete', + value: 'PrintComplete', + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'ShipAddr', + placeholder: 'Add Shippping Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Total Tax', + name: 'TotalTax', + description: 'Total amount of tax incurred.', + type: 'number', + default: 0, + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts new file mode 100644 index 0000000000..2768c675f0 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts @@ -0,0 +1,425 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + estimateAdditionalFieldsOptions, +} from './EstimateAdditionalFieldsOptions'; + +export const estimateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Send', + value: 'send', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'estimate', + ], + }, + }, + }, +] as INodeProperties[]; + +export const estimateFields = [ + // ---------------------------------- + // estimate: create + // ---------------------------------- + { + displayName: 'For Customer', + name: 'CustomerRef', + type: 'options', + required: true, + description: 'The ID of the customer who the estimate is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Line', + name: 'Line', + type: 'collection', + placeholder: 'Add Line Item Property', + description: 'Individual line item of a transaction.', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Detail Type', + name: 'DetailType', + type: 'options', + default: 'SalesItemLineDetail', + options: [ + { + name: 'Sales Item Line Detail', + value: 'SalesItemLineDetail', + }, + ], + }, + { + displayName: 'Item', + name: 'itemId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getItems', + }, + }, + { + displayName: 'Amount', + name: 'Amount', + description: 'Monetary amount of the line item.', + type: 'number', + default: 0, + }, + { + displayName: 'Description', + name: 'Description', + description: 'Textual description of the line item.', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Position', + name: 'LineNum', + description: 'Position of the line item relative to others.', + type: 'number', + default: 1, + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'create', + ], + }, + }, + options: estimateAdditionalFieldsOptions, + }, + + // ---------------------------------- + // estimate: delete + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to delete.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // estimate: get + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to retrieve.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + required: true, + default: false, + description: 'Download the estimate as a PDF file.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // estimate: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting estimates. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // estimate: send + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to send.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'The email of the recipient of the estimate.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'send', + ], + }, + }, + }, + + // ---------------------------------- + // estimate: update + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to update.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'update', + ], + }, + }, + // filter out fields that cannot be updated + options: estimateAdditionalFieldsOptions.filter(property => property.name !== 'TotalAmt' && property.name !== 'TotalTax'), + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..49a70e085e --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts @@ -0,0 +1,183 @@ +export const invoiceAdditionalFieldsOptions = [ + { + displayName: 'Balance', + name: 'Balance', + description: 'The balance reflecting any payments made against the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Billing Email', + name: 'BillEmail', + description: 'E-mail address to which the invoice will be sent.', + type: 'string', + default: '', + }, + { + displayName: 'Customer Memo', + name: 'CustomerMemo', + description: 'User-entered message to the customer. This message is visible to end user on their transactions.', + type: 'string', + default: '', + }, + { + displayName: 'Custom Fields', + name: 'CustomFields', + placeholder: 'Add Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'Field', + values: [ + { + displayName: 'Field Definition ID', + name: 'DefinitionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: 'ID of the field to set.', + }, + { + displayName: 'Field Value', + name: 'StringValue', + type: 'string', + default: '', + description: 'Value of the field to set.', + }, + ], + }, + ], + }, + { + displayName: 'Document Number', + name: 'DocNumber', + description: 'Reference number for the transaction.', + type: 'string', + default: '', + }, + { + displayName: 'Due Date', + name: 'DueDate', + description: 'Date when the payment of the transaction is due.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Email Status', + name: 'EmailStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Send', + value: 'NeedToSend', + }, + { + name: 'Email Sent', + value: 'EmailSent', + }, + ], + }, + { + displayName: 'Print Status', + name: 'PrintStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Print', + value: 'NeedToPrint', + }, + { + name: 'PrintComplete', + value: 'PrintComplete', + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'ShipAddr', + type: 'string', + default: '', + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts new file mode 100644 index 0000000000..d9a4eee478 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts @@ -0,0 +1,451 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + invoiceAdditionalFieldsOptions +} from './InvoiceAdditionalFieldsOptions'; + +export const invoiceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Send', + value: 'send', + }, + { + name: 'Update', + value: 'update', + }, + { + name: 'Void', + value: 'void', + }, + ], + displayOptions: { + show: { + resource: [ + 'invoice', + ], + }, + }, + }, +] as INodeProperties[]; + +export const invoiceFields = [ + // ---------------------------------- + // invoice: create + // ---------------------------------- + { + displayName: 'For Customer', + name: 'CustomerRef', + type: 'options', + required: true, + description: 'The ID of the customer who the invoice is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Line', + name: 'Line', + type: 'collection', + placeholder: 'Add Line Item Property', + description: 'Individual line item of a transaction.', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Detail Type', + name: 'DetailType', + type: 'options', + default: 'SalesItemLineDetail', + options: [ + { + name: 'Sales Item Line Detail', + value: 'SalesItemLineDetail', + }, + ], + }, + { + displayName: 'Item', + name: 'itemId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getItems', + }, + }, + { + displayName: 'Amount', + name: 'Amount', + description: 'Monetary amount of the line item.', + type: 'number', + default: 0, + }, + { + displayName: 'Description', + name: 'Description', + description: 'Textual description of the line item.', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Position', + name: 'LineNum', + description: 'Position of the line item relative to others.', + type: 'number', + default: 1, + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + options: invoiceAdditionalFieldsOptions, + }, + + // ---------------------------------- + // invoice: delete + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to delete.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: get + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to retrieve.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + required: true, + default: false, + description: 'Download the invoice as a PDF file.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // invoice: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting invoices. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: send + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to send.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'The email of the recipient of the invoice.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'send', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: void + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to void.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'void', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: update + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to update.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + // filter out fields that cannot be updated + options: invoiceAdditionalFieldsOptions.filter(property => property.name !== 'TotalAmt' && property.name !== 'Balance'), + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts new file mode 100644 index 0000000000..91a23cabe8 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts @@ -0,0 +1,129 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const itemOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'item', + ], + }, + }, + }, +] as INodeProperties[]; + +export const itemFields = [ + // ---------------------------------- + // item: get + // ---------------------------------- + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + required: true, + default: '', + description: 'The ID of the item to retrieve.', + displayOptions: { + show: { + resource: [ + 'item', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // item: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'item', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'item', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting items. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'item', + ], + operation: [ + 'getAll', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..e683fd2e2b --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts @@ -0,0 +1,9 @@ +export const paymentAdditionalFieldsOptions = [ + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts new file mode 100644 index 0000000000..9b6c58bc85 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts @@ -0,0 +1,399 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + paymentAdditionalFieldsOptions +} from './PaymentAdditionalFieldsOptions'; + +export const paymentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Send', + value: 'send', + }, + { + name: 'Update', + value: 'update', + }, + { + name: 'Void', + value: 'void', + }, + ], + displayOptions: { + show: { + resource: [ + 'payment', + ], + }, + }, + }, +] as INodeProperties[]; + +export const paymentFields = [ + // ---------------------------------- + // payment: create + // ---------------------------------- + { + displayName: 'For Customer ID', + name: 'CustomerRef', + type: 'options', + required: true, + description: 'The ID of the customer who the payment is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'create', + ], + }, + }, + options: paymentAdditionalFieldsOptions, + }, + + // ---------------------------------- + // payment: delete + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to delete.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // payment: get + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to retrieve.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + required: true, + default: false, + description: 'Download estimate as PDF file', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // payment: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting payments. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // payment: send + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to send.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'The email of the recipient of the payment.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'send', + ], + }, + }, + }, + + // ---------------------------------- + // payment: void + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to void.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'void', + ], + }, + }, + }, + + // ---------------------------------- + // payment: update + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to update.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'update', + ], + }, + }, + options: paymentAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts new file mode 100644 index 0000000000..b3994c66eb --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts @@ -0,0 +1,48 @@ +export interface BillingAddress { + Line4: string; + Line3: string; + Line2: string; + Line1: string; + Long: string; + Lat: string; +} + +export interface BillEmail { + Address: string; +} + +export interface CustomField { + DefinitionId: string; + Name: string; +} + +export interface CustomerMemo { + value: string; +} + +export interface GeneralAddress { + City: string; + Line1: string; + PostalCode: string; + Lat: string; + Long: string; + CountrySubDivisionCode: string; +} + +export interface LinkedTxn { + TxnId: string; + TxnType: string; +} + +export interface PrimaryEmailAddr { + Address: string; +} + +export interface PrimaryPhone { + FreeFormNumber: string; +} + +export interface Ref { + value: string; + name?: string; +} diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..68b27a208b --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts @@ -0,0 +1,117 @@ +export const vendorAdditionalFieldsOptions = [ + { + displayName: 'Account Number', + name: 'AcctNum', + type: 'string', + default: '', + }, + { + displayName: 'Active', + name: 'Active', + description: 'Whether the employee is currently enabled for use by QuickBooks.', + type: 'boolean', + default: false, + }, + { + displayName: 'Balance', + name: 'Balance', + description: 'The balance reflecting any payments made against the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Company Name', + name: 'CompanyName', + type: 'string', + default: '', + }, + { + displayName: 'Family Name', + name: 'FamilyName', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'GivenName', + type: 'string', + default: '', + }, + { + displayName: 'Primary Email Address', + name: 'PrimaryEmailAddr', + type: 'string', + default: '', + }, + { + displayName: 'Primary Phone', + name: 'PrimaryPhone', + type: 'string', + default: '', + }, + { + displayName: 'Print-On-Check Name', + name: 'PrintOnCheckName', + description: 'Name of the vendor as printed on a check.', + type: 'string', + default: '', + }, + { + displayName: 'Vendor 1099', + name: 'Vendor1099', + description: 'Whether the vendor is an independent contractor, given a 1099-MISC form at the end of the year.', + type: 'boolean', + default: false, + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts new file mode 100644 index 0000000000..09c1030912 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts @@ -0,0 +1,222 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + vendorAdditionalFieldsOptions, +} from './VendorAdditionalFieldsOptions'; + +export const vendorOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'vendor', + ], + }, + }, + }, +] as INodeProperties[]; + +export const vendorFields = [ + // ---------------------------------- + // vendor: create + // ---------------------------------- + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + required: true, + default: '', + description: 'The display name of the vendor to create.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'create', + ], + }, + }, + options: vendorAdditionalFieldsOptions, + }, + + // ---------------------------------- + // vendor: get + // ---------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + required: true, + default: '', + description: 'The ID of the vendor to retrieve.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // vendor: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting vendors. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // vendor: update + // ---------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + required: true, + default: '', + description: 'The ID of the vendor to update.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'update', + ], + }, + }, + options: vendorAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts new file mode 100644 index 0000000000..d8573f7223 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts @@ -0,0 +1,8 @@ +export * from './Bill/BillDescription'; +export * from './Customer/CustomerDescription'; +export * from './Employee/EmployeeDescription'; +export * from './Estimate/EstimateDescription'; +export * from './Invoice/InvoiceDescription'; +export * from './Item/ItemDescription'; +export * from './Payment/PaymentDescription'; +export * from './Vendor/VendorDescription'; diff --git a/packages/nodes-base/nodes/QuickBooks/quickbooks.svg b/packages/nodes-base/nodes/QuickBooks/quickbooks.svg new file mode 100644 index 0000000000..ac04a2e791 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/quickbooks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/RabbitMQ/DefaultOptions.ts b/packages/nodes-base/nodes/RabbitMQ/DefaultOptions.ts index 10d295dc03..bab50fa743 100644 --- a/packages/nodes-base/nodes/RabbitMQ/DefaultOptions.ts +++ b/packages/nodes-base/nodes/RabbitMQ/DefaultOptions.ts @@ -36,6 +36,37 @@ export const rabbitDefaultOptions: Array { - let channel; + let channel, options: IDataObject; try { const items = this.getInputData(); const mode = this.getNodeParameter('mode', 0) as string; @@ -266,7 +297,7 @@ export class RabbitMQ implements INodeType { if (mode === 'queue') { const queue = this.getNodeParameter('queue', 0) as string; - const options = this.getNodeParameter('options', 0, {}) as IDataObject; + options = this.getNodeParameter('options', 0, {}) as IDataObject; channel = await rabbitmqConnectQueue.call(this, queue, options); @@ -282,7 +313,17 @@ export class RabbitMQ implements INodeType { message = this.getNodeParameter('message', i) as string; } - queuePromises.push(channel.sendToQueue(queue, Buffer.from(message))); + let headers: IDataObject = {}; + if (options.headers && ((options.headers as IDataObject).header! as IDataObject[]).length) { + const itemOptions = this.getNodeParameter('options', i, {}) as IDataObject; + const additionalHeaders: IDataObject = {}; + ((itemOptions.headers as IDataObject).header as IDataObject[]).forEach((header: IDataObject) => { + additionalHeaders[header.key as string] = header.value; + }); + headers = additionalHeaders; + } + + queuePromises.push(channel.sendToQueue(queue, Buffer.from(message), { headers })); } // @ts-ignore @@ -320,7 +361,7 @@ export class RabbitMQ implements INodeType { const type = this.getNodeParameter('exchangeType', 0) as string; const routingKey = this.getNodeParameter('routingKey', 0) as string; - const options = this.getNodeParameter('options', 0, {}) as IDataObject; + options = this.getNodeParameter('options', 0, {}) as IDataObject; channel = await rabbitmqConnectExchange.call(this, exchange, type, options); @@ -336,7 +377,17 @@ export class RabbitMQ implements INodeType { message = this.getNodeParameter('message', i) as string; } - exchangePromises.push(channel.publish(exchange, routingKey, Buffer.from(message))); + let headers: IDataObject = {}; + if (options.headers && ((options.headers as IDataObject).header! as IDataObject[]).length) { + const itemOptions = this.getNodeParameter('options', i, {}) as IDataObject; + const additionalHeaders: IDataObject = {}; + ((itemOptions.headers as IDataObject).header as IDataObject[]).forEach((header: IDataObject) => { + additionalHeaders[header.key as string] = header.value; + }); + headers = additionalHeaders; + } + + exchangePromises.push(channel.publish(exchange, routingKey, Buffer.from(message), { headers })); } // @ts-ignore diff --git a/packages/nodes-base/nodes/Raindrop/GenericFunctions.ts b/packages/nodes-base/nodes/Raindrop/GenericFunctions.ts new file mode 100644 index 0000000000..ff50006513 --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/GenericFunctions.ts @@ -0,0 +1,63 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to Raindrop. + */ +export async function raindropApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject, + option: IDataObject = {}, +) { + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'Content-Type': 'application/json', + }, + method, + uri: `https://api.raindrop.io/rest/v1${endpoint}`, + qs, + body, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + try { + return await this.helpers.requestOAuth2!.call(this, 'raindropOAuth2Api', options); + + } catch (error) { + + if (error?.response?.body?.errorMessage) { + const errorMessage = error?.response?.body?.errorMessage; + throw new Error(`Raindrop error response [${error.statusCode}]: ${errorMessage}`); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts b/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts new file mode 100644 index 0000000000..7792df40a8 --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts @@ -0,0 +1,452 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + isEmpty, + omit, +} from 'lodash'; + +import { + raindropApiRequest, +} from './GenericFunctions'; + +import { + bookmarkFields, + bookmarkOperations, + collectionFields, + collectionOperations, + tagFields, + tagOperations, + userFields, + userOperations, +} from './descriptions'; + +export class Raindrop implements INodeType { + description: INodeTypeDescription = { + displayName: 'Raindrop', + name: 'raindrop', + icon: 'file:raindrop.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Raindrop API', + defaults: { + name: 'Raindrop', + color: '#1988e0', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'raindropOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Bookmark', + value: 'bookmark', + }, + { + name: 'Collection', + value: 'collection', + }, + { + name: 'Tag', + value: 'tag', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'collection', + description: 'Resource to consume', + }, + ...bookmarkOperations, + ...bookmarkFields, + ...collectionOperations, + ...collectionFields, + ...tagOperations, + ...tagFields, + ...userOperations, + ...userFields, + ], + }; + + methods = { + loadOptions: { + async getCollections(this: ILoadOptionsFunctions) { + const responseData = await raindropApiRequest.call(this, 'GET', '/collections', {}, {}); + return responseData.items.map((item: { title: string, _id: string }) => ({ + name: item.title, + value: item._id, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + if (resource === 'bookmark') { + + // ********************************************************************* + // bookmark + // ********************************************************************* + + // https://developer.raindrop.io/v1/raindrops + + if (operation === 'create') { + + // ---------------------------------- + // bookmark: create + // ---------------------------------- + + const body: IDataObject = { + link: this.getNodeParameter('link', i), + collection: { + '$id': this.getNodeParameter('collectionId', i), + }, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (!isEmpty(additionalFields)) { + Object.assign(body, additionalFields); + } + + if (additionalFields.pleaseParse === true) { + body.pleaseParse = {}; + delete additionalFields.pleaseParse; + } + + if (additionalFields.tags) { + body.tags = (additionalFields.tags as string).split(',').map(tag => tag.trim()) as string[]; + } + + const endpoint = `/raindrop`; + responseData = await raindropApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData.item; + + } else if (operation === 'delete') { + + // ---------------------------------- + // bookmark: delete + // ---------------------------------- + + const bookmarkId = this.getNodeParameter('bookmarkId', i); + const endpoint = `/raindrop/${bookmarkId}`; + responseData = await raindropApiRequest.call(this, 'DELETE', endpoint, {}, {}); + + } else if (operation === 'get') { + + // ---------------------------------- + // bookmark: get + // ---------------------------------- + + const bookmarkId = this.getNodeParameter('bookmarkId', i); + const endpoint = `/raindrop/${bookmarkId}`; + responseData = await raindropApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData.item; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // bookmark: getAll + // ---------------------------------- + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const collectionId = this.getNodeParameter('collectionId', i); + const endpoint = `/raindrops/${collectionId}`; + responseData = await raindropApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData.items; + + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.slice(0, limit); + } + + } else if (operation === 'update') { + + // ---------------------------------- + // bookmark: update + // ---------------------------------- + + const bookmarkId = this.getNodeParameter('bookmarkId', i); + + const body = {} as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + Object.assign(body, updateFields); + + if (updateFields.collectionId) { + body.collection = { + '$id': updateFields.collectionId, + }; + delete updateFields.collectionId; + } + + if (updateFields.tags) { + body.tags = (updateFields.tags as string).split(',').map(tag => tag.trim()) as string[]; + } + + const endpoint = `/raindrop/${bookmarkId}`; + responseData = await raindropApiRequest.call(this, 'PUT', endpoint, {}, body); + responseData = responseData.item; + } + } else if (resource === 'collection') { + + // ********************************************************************* + // collection + // ********************************************************************* + + // https://developer.raindrop.io/v1/collections/methods + + if (operation === 'create') { + + // ---------------------------------- + // collection: create + // ---------------------------------- + + const body = { + title: this.getNodeParameter('title', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (!isEmpty(additionalFields)) { + Object.assign(body, additionalFields); + } + + if (additionalFields.cover) { + body.cover = [body.cover]; + } + + if (additionalFields.parentId) { + body['parent.$id'] = parseInt(additionalFields.parentId as string, 10) as number; + delete additionalFields.parentId; + } + + responseData = await raindropApiRequest.call(this, 'POST', `/collection`, {}, body); + responseData = responseData.item; + + } else if (operation === 'delete') { + + // ---------------------------------- + // collection: delete + // ---------------------------------- + + const collectionId = this.getNodeParameter('collectionId', i); + const endpoint = `/collection/${collectionId}`; + responseData = await raindropApiRequest.call(this, 'DELETE', endpoint, {}, {}); + + } else if (operation === 'get') { + + // ---------------------------------- + // collection: get + // ---------------------------------- + + const collectionId = this.getNodeParameter('collectionId', i); + const endpoint = `/collection/${collectionId}`; + responseData = await raindropApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData.item; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // collection: getAll + // ---------------------------------- + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const endpoint = this.getNodeParameter('type', i) === 'parent' + ? '/collections' + : '/collections/childrens'; + + responseData = await raindropApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData.items; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.slice(0, limit); + } + + } else if (operation === 'update') { + + // ---------------------------------- + // collection: update + // ---------------------------------- + + const collectionId = this.getNodeParameter('collectionId', i); + + const body = {} as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + if (updateFields.parentId) { + body['parent.$id'] = parseInt(updateFields.parentId as string, 10) as number; + delete updateFields.parentId; + } + + Object.assign(body, omit(updateFields, 'binaryPropertyName')); + + const endpoint = `/collection/${collectionId}`; + responseData = await raindropApiRequest.call(this, 'PUT', endpoint, {}, body); + responseData = responseData.item; + + // cover-specific endpoint + + if (updateFields.cover) { + + if (!items[i].binary) { + throw new Error('No binary data exists on item!'); + } + + if (!updateFields.cover) { + throw new Error('Please enter a binary property to upload a cover image.'); + } + + const binaryPropertyName = updateFields.cover as string; + + const binaryData = items[i].binary![binaryPropertyName] as IBinaryData; + + const formData = { + cover: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }, + }; + + const endpoint = `/collection/${collectionId}/cover`; + responseData = await raindropApiRequest.call(this, 'PUT', endpoint, {}, {}, { 'Content-Type': 'multipart/form-data', formData }); + responseData = responseData.item; + } + } + + } else if (resource === 'user') { + + // ********************************************************************* + // user + // ********************************************************************* + + // https://developer.raindrop.io/v1/user + + if (operation === 'get') { + + // ---------------------------------- + // user: get + // ---------------------------------- + + const self = this.getNodeParameter('self', i); + let endpoint = '/user'; + + if (self === false) { + const userId = this.getNodeParameter('userId', i); + endpoint += `/${userId}`; + } + + responseData = await raindropApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData.user; + + } + + } else if (resource === 'tag') { + + // ********************************************************************* + // tag + // ********************************************************************* + + // https://developer.raindrop.io/v1/tags + + if (operation === 'delete') { + + // ---------------------------------- + // tag: delete + // ---------------------------------- + + let endpoint = `/tags`; + + const body: IDataObject = { + tags: (this.getNodeParameter('tags', i) as string).split(',') as string[], + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.collectionId) { + endpoint += `/${additionalFields.collectionId}`; + } + + responseData = await raindropApiRequest.call(this, 'DELETE', endpoint, {}, body); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // tag: getAll + // ---------------------------------- + + let endpoint = `/tags`; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const filter = this.getNodeParameter('filters', i) as IDataObject; + + if (filter.collectionId) { + endpoint += `/${filter.collectionId}`; + } + + responseData = await raindropApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData.items; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.slice(0, limit); + } + } + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + + } +} diff --git a/packages/nodes-base/nodes/Raindrop/descriptions/BookmarkDescription.ts b/packages/nodes-base/nodes/Raindrop/descriptions/BookmarkDescription.ts new file mode 100644 index 0000000000..2fc575df85 --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/descriptions/BookmarkDescription.ts @@ -0,0 +1,320 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const bookmarkOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + }, + }, + }, +] as INodeProperties[]; + +export const bookmarkFields = [ + // ---------------------------------- + // bookmark: create + // ---------------------------------- + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'options', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getCollections', + }, + default: '', + }, + { + displayName: 'Link', + name: 'link', + type: 'string', + required: true, + default: '', + description: 'Link of the bookmark to be created.', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Important', + name: 'important', + type: 'boolean', + default: false, + description: 'Whether this bookmark is marked as favorite.', + }, + { + displayName: 'Order', + name: 'order', + type: 'number', + default: 0, + description: 'Sort order for the bookmark. For example, to move it to first place, enter 0.', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: 'Bookmark tags. Multiple can be set separated by comma.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the bookmark to create.', + }, + ], + }, + + // ---------------------------------- + // bookmark: delete + // ---------------------------------- + { + displayName: 'Bookmark ID', + name: 'bookmarkId', + type: 'string', + default: '', + required: true, + description: 'The ID of the bookmark to delete.', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // bookmark: get + // ---------------------------------- + { + displayName: 'Bookmark ID', + name: 'bookmarkId', + type: 'string', + default: '', + required: true, + description: 'The ID of the bookmark to retrieve.', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // bookmark: getAll + // ---------------------------------- + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCollections', + }, + default: [], + required: true, + description: 'The ID of the collection from which to retrieve all bookmarks.', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + 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: [ + 'bookmark', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, + + // ---------------------------------- + // bookmark: update + // ---------------------------------- + { + displayName: 'Bookmark ID', + name: 'bookmarkId', + type: 'string', + default: '', + required: true, + description: 'The ID of the bookmark to update.', + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'bookmark', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCollections', + }, + default: '', + }, + { + displayName: 'Important', + name: 'important', + type: 'boolean', + default: false, + description: 'Whether this bookmark is marked as favorite.', + }, + { + displayName: 'Order', + name: 'order', + type: 'number', + default: 0, + description: 'For example if you want to move bookmark to the first place set this field to 0', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: 'Bookmark tags. Multiple can be set separated by comma.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the bookmark to be created.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Raindrop/descriptions/CollectionDescription.ts b/packages/nodes-base/nodes/Raindrop/descriptions/CollectionDescription.ts new file mode 100644 index 0000000000..3f5cb17dbb --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/descriptions/CollectionDescription.ts @@ -0,0 +1,358 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const collectionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'collection', + ], + }, + }, + }, +] as INodeProperties[]; + +export const collectionFields = [ + // ---------------------------------- + // collection: create + // ---------------------------------- + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + description: 'Title of the collection to create.', + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Cover', + name: 'cover', + type: 'string', + default: '', + description: 'URL of an image to use as cover for the collection.', + }, + { + displayName: 'Public', + name: 'public', + type: 'boolean', + default: false, + description: 'Whether the collection will be accessible without authentication.', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: 'ID of this collection\'s parent collection, if it is a child collection.', + }, + { + displayName: 'Sort Order', + name: 'sort', + type: 'number', + default: 1, + description: 'Descending sort order of this collection. The number is the position of the collection
among all the collections with the same parent ID.', + }, + { + displayName: 'View', + name: 'view', + type: 'options', + default: 'list', + description: 'View style of this collection.', + options: [ + { + name: 'List', + value: 'list', + }, + { + name: 'Simple', + value: 'simple', + }, + { + name: 'Grid', + value: 'grid', + }, + { + name: 'Masonry', + value: 'Masonry', + }, + ], + }, + ], + }, + + // ---------------------------------- + // collection: delete + // ---------------------------------- + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'string', + default: '', + required: true, + description: 'The ID of the collection to delete.', + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // collection: get + // ---------------------------------- + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'string', + default: '', + required: true, + description: 'The ID of the collection to retrieve.', + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // collection: getAll + // ---------------------------------- + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + default: 'parent', + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + name: 'Parent', + value: 'parent', + description: 'Root-level collections.', + }, + { + name: 'Children', + value: 'children', + description: 'Nested collections.', + }, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'collection', + ], + 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: [ + 'collection', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, + + // ---------------------------------- + // collection: update + // ---------------------------------- + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'string', + default: '', + required: true, + description: 'The ID of the collection to update.', + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'collection', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Cover', + name: 'cover', + type: 'string', + default: 'data', + placeholder: '', + description: 'Name of the binary property containing the data
for the image to upload as a cover.', + }, + { + displayName: 'Public', + name: 'public', + type: 'boolean', + default: false, + description: 'Whether the collection will be accessible without authentication.', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: 'ID of this collection\'s parent collection, if it is a child collection.', + }, + { + displayName: 'Sort Order', + name: 'sort', + type: 'number', + default: 1, + description: 'Descending sort order of this collection. The number is the position of the collection
among all the collections with the same parent ID.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the collection to update.', + }, + { + displayName: 'View', + name: 'view', + type: 'options', + default: 'list', + description: 'View style of this collection.', + options: [ + { + name: 'List', + value: 'list', + }, + { + name: 'Simple', + value: 'simple', + }, + { + name: 'Grid', + value: 'grid', + }, + { + name: 'Masonry', + value: 'Masonry', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Raindrop/descriptions/TagDescription.ts b/packages/nodes-base/nodes/Raindrop/descriptions/TagDescription.ts new file mode 100644 index 0000000000..2579be7ca8 --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/descriptions/TagDescription.ts @@ -0,0 +1,155 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'tag', + ], + }, + }, + }, +] as INodeProperties[]; + +export const tagFields = [ + // ---------------------------------- + // tag: delete + // ---------------------------------- + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'One or more tags to delete. Enter comma-separated values to delete multiple tags.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'delete', + ], + }, + }, + options: [ + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCollections', + }, + default: '', + description: `It's possible to restrict remove action to just one collection. It's optional`, + }, + ], + }, + // ---------------------------------- + // tag: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'tag', + ], + 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: [ + 'tag', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Collection ID', + name: 'collectionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCollections', + }, + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Raindrop/descriptions/UserDescription.ts b/packages/nodes-base/nodes/Raindrop/descriptions/UserDescription.ts new file mode 100644 index 0000000000..8d452487a3 --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/descriptions/UserDescription.ts @@ -0,0 +1,71 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + }, +] as INodeProperties[]; + +export const userFields = [ + // ---------------------------------- + // user: get + // ---------------------------------- + { + displayName: 'Self', + name: 'self', + type: 'boolean', + default: true, + required: true, + description: 'Whether to return details on the logged-in user.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + required: true, + description: 'The ID of the user to retrieve.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + self: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Raindrop/descriptions/index.ts b/packages/nodes-base/nodes/Raindrop/descriptions/index.ts new file mode 100644 index 0000000000..42ab082f8a --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/descriptions/index.ts @@ -0,0 +1,4 @@ +export * from './BookmarkDescription'; +export * from './CollectionDescription'; +export * from './TagDescription'; +export * from './UserDescription'; diff --git a/packages/nodes-base/nodes/Raindrop/raindrop.svg b/packages/nodes-base/nodes/Raindrop/raindrop.svg new file mode 100644 index 0000000000..c226932017 --- /dev/null +++ b/packages/nodes-base/nodes/Raindrop/raindrop.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/ReadBinaryFile.node.ts b/packages/nodes-base/nodes/ReadBinaryFile.node.ts index ee38fc0cdc..bfabc72ba3 100644 --- a/packages/nodes-base/nodes/ReadBinaryFile.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFile.node.ts @@ -1,4 +1,4 @@ -import { IExecuteSingleFunctions } from 'n8n-core'; +import { IExecuteFunctions } from 'n8n-core'; import { INodeExecutionData, INodeType, @@ -49,38 +49,46 @@ export class ReadBinaryFile implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; - const filePath = this.getNodeParameter('filePath') as string; + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - let data; - try { - data = await fsReadFileAsync(filePath) as Buffer; - } catch (error) { - if (error.code === 'ENOENT') { - throw new Error(`The file "${filePath}" could not be found.`); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + item = items[itemIndex]; + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; + const filePath = this.getNodeParameter('filePath', itemIndex) as string; + + let data; + try { + data = await fsReadFileAsync(filePath) as Buffer; + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`The file "${filePath}" could not be found.`); + } + + throw error; } - throw error; + const newItem: INodeExecutionData = { + json: item.json, + binary: {}, + }; + + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, item.binary); + } + + newItem.binary![dataPropertyName] = await this.helpers.prepareBinaryData(data, filePath); + returnData.push(newItem); } - const newItem: INodeExecutionData = { - json: item.json, - binary: {}, - }; - - if (item.binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - Object.assign(newItem.binary, item.binary); - } - - newItem.binary![dataPropertyName] = await this.helpers.prepareBinaryData(data, filePath); - - return newItem; + return this.prepareOutputData(returnData); } } diff --git a/packages/nodes-base/nodes/ReadBinaryFiles.node.ts b/packages/nodes-base/nodes/ReadBinaryFiles.node.ts index 4e8d9f2ac3..7a42764f35 100644 --- a/packages/nodes-base/nodes/ReadBinaryFiles.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFiles.node.ts @@ -60,14 +60,12 @@ export class ReadBinaryFiles implements INodeType { const items: INodeExecutionData[] = []; let item: INodeExecutionData; let data: Buffer; - let fileName: string; for (const filePath of files) { data = await fsReadFileAsync(filePath) as Buffer; - fileName = path.parse(filePath).base; item = { binary: { - [dataPropertyName]: await this.helpers.prepareBinaryData(data, fileName), + [dataPropertyName]: await this.helpers.prepareBinaryData(data, filePath), }, json: {}, }; diff --git a/packages/nodes-base/nodes/ReadPdf.node.ts b/packages/nodes-base/nodes/ReadPdf.node.ts index 785683b8ae..953f912c42 100644 --- a/packages/nodes-base/nodes/ReadPdf.node.ts +++ b/packages/nodes-base/nodes/ReadPdf.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; import { @@ -37,22 +37,30 @@ export class ReadPdf implements INodeType { ], }; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - async executeSingle(this: IExecuteSingleFunctions): Promise { + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const binaryPropertyName = this.getNodeParameter('binaryPropertyName') as string; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { - const item = this.getInputData(); + item = items[itemIndex]; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex) as string; + + if (item.binary === undefined) { + item.binary = {}; + } + + const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); + returnData.push({ + binary: item.binary, + json: await pdf(binaryData), + }); - if (item.binary === undefined) { - item.binary = {}; } - - const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); - - return { - binary: item.binary, - json: await pdf(binaryData), - }; + return this.prepareOutputData(returnData); } + } diff --git a/packages/nodes-base/nodes/Reddit/PostDescription.ts b/packages/nodes-base/nodes/Reddit/PostDescription.ts index cce6a0f582..a5c9606874 100644 --- a/packages/nodes-base/nodes/Reddit/PostDescription.ts +++ b/packages/nodes-base/nodes/Reddit/PostDescription.ts @@ -30,6 +30,11 @@ export const postOperations = [ value: 'getAll', description: 'Get all posts from a subreddit', }, + { + name: 'Search', + value: 'search', + description: 'Search posts in a subreddit or in all of Reddit.', + }, ], displayOptions: { show: { @@ -348,4 +353,166 @@ export const postFields = [ }, ], }, + + // ---------------------------------- + // post: search + // ---------------------------------- + { + displayName: 'Location', + name: 'location', + type: 'options', + default: 'subreddit', + description: 'Location where to search for posts.', + options: [ + { + name: 'All Reddit', + value: 'allReddit', + description: 'Search for posts in all of Reddit.', + }, + { + name: 'Subreddit', + value: 'subreddit', + description: 'Search for posts in a specific subreddit.', + }, + ], + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'search', + ], + }, + }, + }, + { + displayName: 'Subreddit', + name: 'subreddit', + type: 'string', + required: true, + default: '', + description: 'The name of subreddit to search in.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'search', + ], + location: [ + 'subreddit', + ], + }, + }, + }, + { + displayName: 'Keyword', + name: 'keyword', + type: 'string', + default: '', + required: true, + description: 'The keyword for the search.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'search', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'search', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'search', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'search', + ], + }, + }, + options: [ + { + displayName: 'Sort', + name: 'sort', + placeholder: '', + type: 'options', + default: 'relevance', + description: 'The category to sort results by.', + options: [ + { + name: 'Comments', + value: 'comments', + }, + { + name: 'Hot', + value: 'hot', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Top', + value: 'top', + }, + { + name: 'Relevance', + value: 'relevance', + }, + ], + }, + ], + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Reddit/Reddit.node.json b/packages/nodes-base/nodes/Reddit/Reddit.node.json new file mode 100644 index 0000000000..c0b6e172a0 --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/Reddit.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.reddit", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/reddit" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.reddit/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Reddit/Reddit.node.ts b/packages/nodes-base/nodes/Reddit/Reddit.node.ts index 29d41a39d3..093414e021 100644 --- a/packages/nodes-base/nodes/Reddit/Reddit.node.ts +++ b/packages/nodes-base/nodes/Reddit/Reddit.node.ts @@ -130,6 +130,7 @@ export class Reddit implements INodeType { if (resource === 'post') { if (operation === 'create') { + // ---------------------------------- // post: create // ---------------------------------- @@ -155,6 +156,7 @@ export class Reddit implements INodeType { responseData = responseData.json.data; } else if (operation === 'delete') { + // ---------------------------------- // post: delete // ---------------------------------- @@ -172,6 +174,7 @@ export class Reddit implements INodeType { responseData = { success: true }; } else if (operation === 'get') { + // ---------------------------------- // post: get // ---------------------------------- @@ -184,6 +187,7 @@ export class Reddit implements INodeType { responseData = responseData[0].data.children[0].data; } else if (operation === 'getAll') { + // ---------------------------------- // post: getAll // ---------------------------------- @@ -203,6 +207,45 @@ export class Reddit implements INodeType { responseData = await handleListing.call(this, i, endpoint); + } else if (operation === 'search') { + + // ---------------------------------- + // post: search + // ---------------------------------- + + // https://www.reddit.com/dev/api/#GET_search + + const location = this.getNodeParameter('location', i); + + const qs = { + q: this.getNodeParameter('keyword', i), + restrict_sr: location === 'subreddit', + } as IDataObject; + + const { sort } = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (sort) { + qs.sort = sort; + } + + let endpoint = ''; + + if (location === 'allReddit') { + endpoint = 'search.json'; + } else { + const subreddit = this.getNodeParameter('subreddit', i); + endpoint = `r/${subreddit}/search.json`; + } + + responseData = await handleListing.call(this, i, endpoint, qs); + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + } } else if (resource === 'postComment') { @@ -212,6 +255,7 @@ export class Reddit implements INodeType { // ********************************************************************* if (operation === 'create') { + // ---------------------------------- // postComment: create // ---------------------------------- @@ -229,6 +273,7 @@ export class Reddit implements INodeType { responseData = responseData.json.data.things[0].data; } else if (operation === 'getAll') { + // ---------------------------------- // postComment: getAll // ---------------------------------- @@ -242,6 +287,7 @@ export class Reddit implements INodeType { responseData = await handleListing.call(this, i, endpoint); } else if (operation === 'delete') { + // ---------------------------------- // postComment: delete // ---------------------------------- @@ -259,6 +305,7 @@ export class Reddit implements INodeType { responseData = { success: true }; } else if (operation === 'reply') { + // ---------------------------------- // postComment: reply // ---------------------------------- @@ -277,11 +324,13 @@ export class Reddit implements INodeType { } } else if (resource === 'profile') { + // ********************************************************************* - // pprofile + // profile // ********************************************************************* if (operation === 'get') { + // ---------------------------------- // profile: get // ---------------------------------- @@ -329,6 +378,7 @@ export class Reddit implements INodeType { // ********************************************************************* if (operation === 'get') { + // ---------------------------------- // subreddit: get // ---------------------------------- @@ -349,6 +399,7 @@ export class Reddit implements INodeType { } } else if (operation === 'getAll') { + // ---------------------------------- // subreddit: getAll // ---------------------------------- @@ -389,6 +440,7 @@ export class Reddit implements INodeType { } } else if (resource === 'user') { + // ********************************************************************* // user // ********************************************************************* diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index f7b1bd0bdd..3d1aafa962 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -16,7 +16,7 @@ export class Redis implements INodeType { description: INodeTypeDescription = { displayName: 'Redis', name: 'redis', - icon: 'file:redis.png', + icon: 'file:redis.svg', group: ['input'], version: 1, description: 'Get, send and update data in Redis.', @@ -445,6 +445,7 @@ export class Redis implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; client.on('error', (err: Error) => { + client.quit(); reject(err); }); @@ -518,6 +519,7 @@ export class Redis implements INodeType { } } + client.quit(); resolve(this.prepareOutputData(returnItems)); } }); diff --git a/packages/nodes-base/nodes/Redis/redis.png b/packages/nodes-base/nodes/Redis/redis.png deleted file mode 100644 index 7254774f29..0000000000 Binary files a/packages/nodes-base/nodes/Redis/redis.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Redis/redis.svg b/packages/nodes-base/nodes/Redis/redis.svg new file mode 100644 index 0000000000..252d8c080d --- /dev/null +++ b/packages/nodes-base/nodes/Redis/redis.svg @@ -0,0 +1,17 @@ + + + redis_node_icon + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/RenameKeys.node.ts b/packages/nodes-base/nodes/RenameKeys.node.ts index 7838c80bb6..d427db9cf4 100644 --- a/packages/nodes-base/nodes/RenameKeys.node.ts +++ b/packages/nodes-base/nodes/RenameKeys.node.ts @@ -39,6 +39,7 @@ export class RenameKeys implements INodeType { type: 'fixedCollection', typeOptions: { multipleValues: true, + sortable: true, }, default: {}, options: [ diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts index 51e145b384..2078470d13 100644 --- a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -1,12 +1,14 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, - IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, } from 'n8n-core'; -export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resource: string, method: string, operation: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any +export async function rocketchatApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, resource: string, method: string, operation: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('rocketchatApi'); if (credentials === undefined) { @@ -14,7 +16,11 @@ export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFuncti } const headerWithAuthentication = Object.assign({}, headers, - { 'X-Auth-Token': credentials.authKey, 'X-User-Id': credentials.userId }); + { + 'X-Auth-Token': credentials.authKey, + 'X-User-Id': credentials.userId, + }, + ); const options: OptionsWithUri = { headers: headerWithAuthentication, @@ -29,13 +35,15 @@ export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFuncti try { return await this.helpers.request!(options); } catch (error) { - let errorMessage = error.message; + if (error.response && error.response.body && error.response.body.error) { - if (error.response.body.error) { - errorMessage = error.response.body.error; + const errorMessage = error.response.body.error; + // Try to return the error prettier + throw new Error( + `Rocketchat error response [${error.statusCode}]: ${errorMessage}`, + ); } - - throw new Error(`Rocket.chat error response [${error.statusCode}]: ${errorMessage}`); + throw error; } } diff --git a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts index d9bc0cf491..f027fdc1db 100644 --- a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts +++ b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts @@ -1,13 +1,15 @@ import { - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; - import { + +import { rocketchatApiRequest, validateJSON } from './GenericFunctions'; @@ -50,7 +52,7 @@ export class Rocketchat implements INodeType { description: INodeTypeDescription = { displayName: 'RocketChat', name: 'rocketchat', - icon: 'file:rocketchat.png', + icon: 'file:rocketchat.svg', group: ['output'], version: 1, subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', @@ -395,106 +397,113 @@ export class Rocketchat implements INodeType { ], }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const resource = this.getNodeParameter('resource') as string; - const operation = this.getNodeParameter('operation') as string; - let response; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = (items.length as unknown) as number; + let responseData; + const returnData: IDataObject[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'chat') { + //https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage + if (operation === 'postMessage') { + const channel = this.getNodeParameter('channel', i) as string; + const text = this.getNodeParameter('text', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean; - if (resource === 'chat') { - //https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage - if (operation === 'postMessage') { - const channel = this.getNodeParameter('channel') as string; - const text = this.getNodeParameter('text') as string; - const options = this.getNodeParameter('options') as IDataObject; - const jsonActive = this.getNodeParameter('jsonParameters') as boolean; + const body: IPostMessageBody = { + channel, + text, + }; - const body: IPostMessageBody = { - channel, - text, - }; + if (options.alias) { + body.alias = options.alias as string; + } + if (options.avatar) { + body.avatar = options.avatar as string; + } + if (options.emoji) { + body.emoji = options.emoji as string; + } - if (options.alias) { - body.alias = options.alias as string; - } - if (options.avatar) { - body.avatar = options.avatar as string; - } - if (options.emoji) { - body.emoji = options.emoji as string; - } - - if (!jsonActive) { - const optionsAttachments = this.getNodeParameter('attachments') as IDataObject[]; - if (optionsAttachments.length > 0) { - const attachments: IAttachment[] = []; - for (let i = 0; i < optionsAttachments.length; i++) { - const attachment: IAttachment = {}; - for (const option of Object.keys(optionsAttachments[i])) { - if (option === 'color') { - attachment.color = optionsAttachments[i][option] as string; - } else if (option === 'text') { - attachment.text = optionsAttachments[i][option] as string; - } else if (option === 'ts') { - attachment.ts = optionsAttachments[i][option] as string; - } else if (option === 'messageLinks') { - attachment.message_link = optionsAttachments[i][option] as string; - } else if (option === 'thumbUrl') { - attachment.thumb_url = optionsAttachments[i][option] as string; - } else if (option === 'collapsed') { - attachment.collapsed = optionsAttachments[i][option] as boolean; - } else if (option === 'authorName') { - attachment.author_name = optionsAttachments[i][option] as string; - } else if (option === 'authorLink') { - attachment.author_link = optionsAttachments[i][option] as string; - } else if (option === 'authorIcon') { - attachment.author_icon = optionsAttachments[i][option] as string; - } else if (option === 'title') { - attachment.title = optionsAttachments[i][option] as string; - } else if (option === 'titleLink') { - attachment.title_link = optionsAttachments[i][option] as string; - } else if (option === 'titleLinkDownload') { - attachment.title_link_download = optionsAttachments[i][option] as boolean; - } else if (option === 'imageUrl') { - attachment.image_url = optionsAttachments[i][option] as string; - } else if (option === 'audioUrl') { - attachment.audio_url = optionsAttachments[i][option] as string; - } else if (option === 'videoUrl') { - attachment.video_url = optionsAttachments[i][option] as string; - } else if (option === 'fields') { - const fieldsValues = (optionsAttachments[i][option] as IDataObject).fieldsValues as IDataObject[]; - if (fieldsValues.length > 0) { - const fields: IField[] = []; - for (let i = 0; i < fieldsValues.length; i++) { - const field: IField = {}; - for (const key of Object.keys(fieldsValues[i])) { - if (key === 'short') { - field.short = fieldsValues[i][key] as boolean; - } else if (key === 'title') { - field.title = fieldsValues[i][key] as string; - } else if (key === 'value') { - field.value = fieldsValues[i][key] as string; + if (!jsonActive) { + const optionsAttachments = this.getNodeParameter('attachments', i) as IDataObject[]; + if (optionsAttachments.length > 0) { + const attachments: IAttachment[] = []; + for (let i = 0; i < optionsAttachments.length; i++) { + const attachment: IAttachment = {}; + for (const option of Object.keys(optionsAttachments[i])) { + if (option === 'color') { + attachment.color = optionsAttachments[i][option] as string; + } else if (option === 'text') { + attachment.text = optionsAttachments[i][option] as string; + } else if (option === 'ts') { + attachment.ts = optionsAttachments[i][option] as string; + } else if (option === 'messageLinks') { + attachment.message_link = optionsAttachments[i][option] as string; + } else if (option === 'thumbUrl') { + attachment.thumb_url = optionsAttachments[i][option] as string; + } else if (option === 'collapsed') { + attachment.collapsed = optionsAttachments[i][option] as boolean; + } else if (option === 'authorName') { + attachment.author_name = optionsAttachments[i][option] as string; + } else if (option === 'authorLink') { + attachment.author_link = optionsAttachments[i][option] as string; + } else if (option === 'authorIcon') { + attachment.author_icon = optionsAttachments[i][option] as string; + } else if (option === 'title') { + attachment.title = optionsAttachments[i][option] as string; + } else if (option === 'titleLink') { + attachment.title_link = optionsAttachments[i][option] as string; + } else if (option === 'titleLinkDownload') { + attachment.title_link_download = optionsAttachments[i][option] as boolean; + } else if (option === 'imageUrl') { + attachment.image_url = optionsAttachments[i][option] as string; + } else if (option === 'audioUrl') { + attachment.audio_url = optionsAttachments[i][option] as string; + } else if (option === 'videoUrl') { + attachment.video_url = optionsAttachments[i][option] as string; + } else if (option === 'fields') { + const fieldsValues = (optionsAttachments[i][option] as IDataObject).fieldsValues as IDataObject[]; + if (fieldsValues.length > 0) { + const fields: IField[] = []; + for (let i = 0; i < fieldsValues.length; i++) { + const field: IField = {}; + for (const key of Object.keys(fieldsValues[i])) { + if (key === 'short') { + field.short = fieldsValues[i][key] as boolean; + } else if (key === 'title') { + field.title = fieldsValues[i][key] as string; + } else if (key === 'value') { + field.value = fieldsValues[i][key] as string; + } } + fields.push(field); + attachment.fields = fields; } - fields.push(field); - attachment.fields = fields; } } } + attachments.push(attachment); } - attachments.push(attachment); + body.attachments = attachments; } - body.attachments = attachments; + } else { + body.attachments = validateJSON(this.getNodeParameter('attachmentsJson', i) as string); } - } else { - body.attachments = validateJSON(this.getNodeParameter('attachmentsJson') as string); - } - response = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); + responseData = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); } } - return { - json: response, - }; + return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/Rocketchat/rocketchat.png b/packages/nodes-base/nodes/Rocketchat/rocketchat.png deleted file mode 100644 index e44281abc8..0000000000 Binary files a/packages/nodes-base/nodes/Rocketchat/rocketchat.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Rocketchat/rocketchat.svg b/packages/nodes-base/nodes/Rocketchat/rocketchat.svg new file mode 100644 index 0000000000..5a72e53f7e --- /dev/null +++ b/packages/nodes-base/nodes/Rocketchat/rocketchat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index dee94afbb6..037b94afc1 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -163,7 +163,12 @@ export function getDefaultFields(sobject: string) { export function getQuery(options: IDataObject, sobject: string, returnAll: boolean, limit = 0) { const fields: string[] = []; if (options.fields) { - fields.push.apply(fields, (options.fields as string).split(',')); + // options.fields is comma separated in standard Salesforce objects and array in custom Salesforce objects -- handle both cases + if (typeof options.fields === 'string') { + fields.push.apply(fields, options.fields.split(',')); + } else { + fields.push.apply(fields, options.fields as string[]); + } } else { fields.push.apply(fields, (getDefaultFields(sobject) as string || 'id').split(',')); } diff --git a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts index 802b20c3bd..402c39792a 100644 --- a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts @@ -229,6 +229,13 @@ export const leadFields = [ default: '', description: 'Source from which the lead was obtained.', }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + type: 'string', + default: '', + description: `Contact’s mobile phone number.`, + }, { displayName: 'Number Of Employees', name: 'numberOfEmployees', @@ -480,6 +487,13 @@ export const leadFields = [ default: '', description: 'Source from which the lead was obtained.', }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + type: 'string', + default: '', + description: `Contact’s mobile phone number.`, + }, { displayName: 'Number Of Employees', name: 'numberOfEmployees', diff --git a/packages/nodes-base/nodes/Salesforce/LeadInterface.ts b/packages/nodes-base/nodes/Salesforce/LeadInterface.ts index 227e5fe255..02196961ed 100644 --- a/packages/nodes-base/nodes/Salesforce/LeadInterface.ts +++ b/packages/nodes-base/nodes/Salesforce/LeadInterface.ts @@ -22,4 +22,5 @@ export interface ILead { AnnualRevenue?: number; IsUnreadByOwner?: boolean; NumberOfEmployees?: number; + MobilePhone?: string; } diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index 3afec9ae58..031280d0b3 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -114,7 +114,7 @@ export class Salesforce implements INodeType { description: INodeTypeDescription = { displayName: 'Salesforce', name: 'salesforce', - icon: 'file:salesforce.png', + icon: 'file:salesforce.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -906,8 +906,8 @@ export class Salesforce implements INodeType { if (additionalFields.industry !== undefined) { body.Industry = additionalFields.industry as string; } - if (additionalFields.firstName !== undefined) { - body.FirstName = additionalFields.firstName as string; + if (additionalFields.firstname !== undefined) { + body.FirstName = additionalFields.firstname as string; } if (additionalFields.leadSource !== undefined) { body.LeadSource = additionalFields.leadSource as string; @@ -930,6 +930,9 @@ export class Salesforce implements INodeType { if (additionalFields.numberOfEmployees !== undefined) { body.NumberOfEmployees = additionalFields.numberOfEmployees as number; } + if (additionalFields.mobilePhone !== undefined) { + body.MobilePhone = additionalFields.mobilePhone as string; + } if (additionalFields.customFieldsUi) { const customFields = (additionalFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; if (customFields) { @@ -939,7 +942,6 @@ export class Salesforce implements INodeType { } } } - responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/lead', body); } //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/patch-lead-id @@ -995,8 +997,8 @@ export class Salesforce implements INodeType { if (updateFields.industry !== undefined) { body.Industry = updateFields.industry as string; } - if (updateFields.firstName !== undefined) { - body.FirstName = updateFields.firstName as string; + if (updateFields.firstname !== undefined) { + body.FirstName = updateFields.firstname as string; } if (updateFields.leadSource !== undefined) { body.LeadSource = updateFields.leadSource as string; @@ -1019,6 +1021,9 @@ export class Salesforce implements INodeType { if (updateFields.numberOfEmployees !== undefined) { body.NumberOfEmployees = updateFields.numberOfEmployees as number; } + if (updateFields.mobilePhone !== undefined) { + body.MobilePhone = updateFields.mobilePhone as string; + } if (updateFields.customFieldsUi) { const customFields = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; if (customFields) { diff --git a/packages/nodes-base/nodes/Salesforce/salesforce.png b/packages/nodes-base/nodes/Salesforce/salesforce.png deleted file mode 100644 index aca78c51ab..0000000000 Binary files a/packages/nodes-base/nodes/Salesforce/salesforce.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Salesforce/salesforce.svg b/packages/nodes-base/nodes/Salesforce/salesforce.svg new file mode 100644 index 0000000000..26f1bf161b --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/salesforce.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/SendGrid/GenericFunctions.ts b/packages/nodes-base/nodes/SendGrid/GenericFunctions.ts index 60612c7de8..0e4c81713e 100644 --- a/packages/nodes-base/nodes/SendGrid/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SendGrid/GenericFunctions.ts @@ -13,7 +13,7 @@ import { IDataObject, } from 'n8n-workflow'; -export async function sendGridApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined): Promise { // tslint:disable-line:no-any +export async function sendGridApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('sendGridApi') as IDataObject; const host = 'api.sendgrid.com/v3'; @@ -25,7 +25,7 @@ export async function sendGridApiRequest(this: IHookFunctions | IExecuteFunction method, qs, body, - uri: uri || `https://${host}${endpoint}`, + uri: `https://${host}${endpoint}`, json: true, }; @@ -33,6 +33,10 @@ export async function sendGridApiRequest(this: IHookFunctions | IExecuteFunction delete options.body; } + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + try { //@ts-ignore return await this.helpers.request!(options); diff --git a/packages/nodes-base/nodes/SendGrid/MailDescription.ts b/packages/nodes-base/nodes/SendGrid/MailDescription.ts new file mode 100644 index 0000000000..6ba317168f --- /dev/null +++ b/packages/nodes-base/nodes/SendGrid/MailDescription.ts @@ -0,0 +1,381 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const mailOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'mail', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send an email.', + }, + ], + default: 'send', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const mailFields = [ + /* -------------------------------------------------------------------------- */ + /* mail:send */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Sender Email', + name: 'fromEmail', + type: 'string', + default: '', + placeholder: 'sender@domain.com', + description: 'Email address of the sender of the email.', + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Sender Name', + name: 'fromName', + type: 'string', + default: '', + placeholder: 'John Smith', + description: 'Name of the sender of the email.', + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Recipient Email', + name: 'toEmail', + type: 'string', + default: '', + placeholder: 'recipient@domain.com', + description: 'Comma-separated list of recipient email addresses.', + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'Subject of the email to send.', + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + dynamicTemplate: [ + false, + ], + }, + }, + }, + { + displayName: 'Dynamic Template', + name: 'dynamicTemplate', + type: 'boolean', + required: true, + default: false, + description: 'Whether this email will contain a dynamic template.', + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'MIME type', + name: 'contentType', + type: 'options', + default: 'text/plain', + description: 'MIME type of the email to send.', + options: [ + { + name: 'Plain Text', + value: 'text/plain', + }, + { + name: 'HTML', + value: 'text/html', + }, + ], + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + dynamicTemplate: [ + false, + ], + }, + }, + }, + { + displayName: 'Message Body', + name: 'contentValue', + type: 'string', + default: '', + required: true, + description: 'Message body of the email to send.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + dynamicTemplate: [ + false, + ], + }, + }, + }, + { + displayName: 'Template ID', + name: 'templateId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getTemplateIds', + }, + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + dynamicTemplate: [ + true, + ], + }, + }, + }, + { + displayName: 'Dynamic Template Fields', + name: 'dynamicTemplateFields', + placeholder: 'Add Dynamic Template Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + dynamicTemplate: [ + true, + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Key of the dynamic template field.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value for the field', + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'mail', + ], + operation: [ + 'send', + ], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'string', + default: '', + description: 'Comma-separated list of binary properties', + }, + { + displayName: 'BCC Email', + name: 'bccEmail', + type: 'string', + default: '', + description: 'Comma-separated list of emails of the recipients
of a blind carbon copy of the email.', + }, + { + displayName: 'Categories', + name: 'categories', + type: 'string', + default: '', + description: 'Comma-separated list of categories. Each category name may not exceed 255 characters.', + }, + { + displayName: 'CC Email', + name: 'ccEmail', + type: 'string', + default: '', + description: 'Comma-separated list of emails of the recipients
of a carbon copy of the email.', + }, + { + displayName: 'Enable Sandbox', + name: 'enableSandbox', + type: 'boolean', + default: false, + description: 'Whether to use to the sandbox for testing out email-sending functionality.', + }, + { + displayName: 'IP Pool Name', + name: 'ipPoolName', + type: 'string', + default: '', + description: 'The IP Pool that you would like to send this email from.', + }, + { + displayName: 'Headers', + name: 'headers', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Key to set in the header object.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set in the header object.', + }, + ], + }, + ], + }, + { + displayName: 'Send At', + name: 'sendAt', + type: 'dateTime', + default: '', + description: 'When to deliver the email. Scheduling more than 72 hours in advance is forbidden.', + }, + ], + }, +] as INodeProperties[]; + +export type SendMailBody = { + personalizations: Array<{ + to: EmailName[], + subject?: string, + cc?: EmailName[], + bcc?: EmailName[], + dynamic_template_data?: { [key: string]: string }, + send_at?: number, + }>, + ip_pool_name?: string; + from: EmailName, + template_id?: string, + content?: Array<{ + type: string, + value: string, + }>, + categories?: string[], + headers?: { [key: string]: string }, + attachments?: Array<{ + content: string, + filename: string, + type: string, + }>, + mail_settings: { + sandbox_mode: { + enable: boolean, + }, + }, +}; + +type EmailName = { + email: string, + name?: string, +}; diff --git a/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts b/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts index 6c4f33645d..9565aa8917 100644 --- a/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts +++ b/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts @@ -1,4 +1,5 @@ import { + BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; @@ -21,11 +22,19 @@ import { contactOperations } from './ContactDescription'; +import { + mailFields, + mailOperations, + SendMailBody, +} from './MailDescription'; + import { sendGridApiRequest, sendGridApiRequestAllItems, } from './GenericFunctions'; +import * as moment from 'moment-timezone'; + export class SendGrid implements INodeType { description: INodeTypeDescription = { displayName: 'SendGrid', @@ -63,6 +72,10 @@ export class SendGrid implements INodeType { name: 'List', value: 'list', }, + { + name: 'Mail', + value: 'mail', + }, ], default: 'list', required: true, @@ -72,17 +85,19 @@ export class SendGrid implements INodeType { ...listFields, ...contactOperations, ...contactFields, + ...mailOperations, + ...mailFields, ], }; - methods ={ + methods = { loadOptions: { // Get custom fields to display to user so that they can select them easily - async getCustomFields(this: ILoadOptionsFunctions,):Promise{ + async getCustomFields(this: ILoadOptionsFunctions,): Promise { const returnData: INodePropertyOptions[] = []; const { custom_fields } = await sendGridApiRequest.call(this, '/marketing/field_definitions', 'GET', {}, {}); if (custom_fields !== undefined) { - for (const customField of custom_fields){ + for (const customField of custom_fields) { returnData.push({ name: customField.name, value: customField.id, @@ -103,6 +118,10 @@ export class SendGrid implements INodeType { } return returnData; }, + async getTemplateIds(this: ILoadOptionsFunctions): Promise { + const responseData = await sendGridApiRequest.call(this, '/templates', 'GET', {}, { generations: 'dynamic' }); + return responseData.templates.map(({ id, name }: { id: string, name: string }) => ({ name, value: id })); + }, }, }; @@ -111,6 +130,7 @@ export class SendGrid implements INodeType { const length = (items.length as unknown) as number; const qs: IDataObject = {}; let responseData; + const timezone = this.getTimezone(); const returnData: IDataObject[] = []; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; @@ -150,7 +170,7 @@ export class SendGrid implements INodeType { const email = this.getNodeParameter('email', i) as string; endpoint = '/marketing/contacts/search'; method = 'POST'; - Object.assign(body, { query: `email LIKE '${email}' `}); + Object.assign(body, { query: `email LIKE '${email}' ` }); } responseData = await sendGridApiRequest.call(this, endpoint, method, body, qs); responseData = responseData.result || responseData; @@ -163,7 +183,7 @@ export class SendGrid implements INodeType { if (operation === 'upsert') { const contacts = []; for (let i = 0; i < length; i++) { - const email = this.getNodeParameter('email',i) as string; + const email = this.getNodeParameter('email', i) as string; const additionalFields = this.getNodeParameter( 'additionalFields', i, @@ -175,7 +195,7 @@ export class SendGrid implements INodeType { const addressValues = (additionalFields.addressUi as IDataObject).addressValues as IDataObject; const addressLine1 = addressValues.address1 as string; const addressLine2 = addressValues.address2 as string; - if (addressLine2){ + if (addressLine2) { Object.assign(contact, { address_line_2: addressLine2 }); } Object.assign(contact, { address_line_1: addressLine1 }); @@ -194,7 +214,7 @@ export class SendGrid implements INodeType { } if (additionalFields.lastName) { const lastName = additionalFields.lastName as string; - Object.assign(contact, { last_name:lastName}); + Object.assign(contact, { last_name: lastName }); } if (additionalFields.postalCode) { const postalCode = additionalFields.postalCode as string; @@ -231,17 +251,17 @@ export class SendGrid implements INodeType { if (operation === 'delete') { for (let i = 0; i < length; i++) { const deleteAll = this.getNodeParameter('deleteAll', i) as boolean; - if(deleteAll === true) { + if (deleteAll === true) { qs.delete_all_contacts = 'true'; } - qs.ids = (this.getNodeParameter('ids',i) as string).replace(/\s/g, ''); + qs.ids = (this.getNodeParameter('ids', i) as string).replace(/\s/g, ''); responseData = await sendGridApiRequest.call(this, `/marketing/contacts`, 'DELETE', {}, qs); returnData.push(responseData); } } } if (resource === 'list') { - if (operation === 'getAll'){ + if (operation === 'getAll') { for (let i = 0; i < length; i++) { const returnAll = this.getNodeParameter('returnAll', i) as boolean; responseData = await sendGridApiRequestAllItems.call(this, `/marketing/lists`, 'GET', 'result', {}, qs); @@ -254,7 +274,7 @@ export class SendGrid implements INodeType { } if (operation === 'get') { for (let i = 0; i < length; i++) { - const listId = this.getNodeParameter('listId',i) as string; + const listId = this.getNodeParameter('listId', i) as string; qs.contact_sample = this.getNodeParameter('contactSample', i) as boolean; responseData = await sendGridApiRequest.call(this, `/marketing/lists/${listId}`, 'GET', {}, qs); returnData.push(responseData); @@ -262,29 +282,155 @@ export class SendGrid implements INodeType { } if (operation === 'create') { for (let i = 0; i < length; i++) { - const name = this.getNodeParameter('name',i) as string; + const name = this.getNodeParameter('name', i) as string; responseData = await sendGridApiRequest.call(this, '/marketing/lists', 'POST', { name }, qs); returnData.push(responseData); } } if (operation === 'delete') { for (let i = 0; i < length; i++) { - const listId = this.getNodeParameter('listId',i) as string; + const listId = this.getNodeParameter('listId', i) as string; qs.delete_contacts = this.getNodeParameter('deleteContacts', i) as boolean; responseData = await sendGridApiRequest.call(this, `/marketing/lists/${listId}`, 'DELETE', {}, qs); responseData = { success: true }; returnData.push(responseData); } } - if (operation=== 'update'){ + if (operation === 'update') { for (let i = 0; i < length; i++) { - const name = this.getNodeParameter('name',i) as string; - const listId = this.getNodeParameter('listId',i) as string; + const name = this.getNodeParameter('name', i) as string; + const listId = this.getNodeParameter('listId', i) as string; responseData = await sendGridApiRequest.call(this, `/marketing/lists/${listId}`, 'PATCH', { name }, qs); returnData.push(responseData); } } } + if (resource === 'mail') { + if (operation === 'send') { + for (let i = 0; i < length; i++) { + + const toEmail = this.getNodeParameter('toEmail', i) as string; + + const parsedToEmail = toEmail.includes(',') + ? toEmail.split(',').map((i) => ({ email: i.trim() })) + : [{ email: toEmail.trim() }]; + + const { + bccEmail, + ccEmail, + enableSandbox, + sendAt, + headers, + attachments, + categories, + ipPoolName, + } = this.getNodeParameter('additionalFields', i) as { + bccEmail: string; + ccEmail: string; + enableSandbox: boolean, + sendAt: string; + headers: { details: Array<{ key: string; value: string }> }; + attachments: string; + categories: string; + ipPoolName: string; + }; + + const body: SendMailBody = { + personalizations: [{ + to: parsedToEmail, + }], + from: { + email: (this.getNodeParameter('fromEmail', i) as string).trim(), + name: this.getNodeParameter('fromName', i) as string, + }, + mail_settings: { + sandbox_mode: { + enable: enableSandbox || false, + }, + }, + }; + + const dynamicTemplateEnabled = this.getNodeParameter('dynamicTemplate', i); + + // dynamic template + if (dynamicTemplateEnabled) { + body.template_id = this.getNodeParameter('templateId', i) as string; + + const { fields } = this.getNodeParameter('dynamicTemplateFields', i) as { + fields: Array<{ [key: string]: string }> + }; + + if (fields) { + body.personalizations[0].dynamic_template_data = {}; + fields.forEach(field => { + body.personalizations[0].dynamic_template_data![field.key] = field.value; + }); + } + + // message body + } else { + body.personalizations[0].subject = this.getNodeParameter('subject', i) as string; + body.content = [{ + type: this.getNodeParameter('contentType', i) as string, + value: this.getNodeParameter('contentValue', i) as string, + }]; + } + + if (attachments) { + const attachmentsToSend = []; + const binaryProperties = attachments.split(',').map((p) => p.trim()); + + for (const property of binaryProperties) { + if (!items[i].binary?.hasOwnProperty(property)) { + throw new Error(`The binary property ${property} does not exist`); + } + + const binaryProperty = items[i].binary![property]; + + attachmentsToSend.push({ + content: binaryProperty.data, + filename: binaryProperty.fileName || 'unknown', + type: binaryProperty.mimeType, + }); + } + + if (attachmentsToSend.length) { + body.attachments = attachmentsToSend; + } + } + + if (bccEmail) { + body.personalizations[0].bcc = bccEmail.split(',').map(i => ({ email: i.trim() })); + } + + if (ccEmail) { + body.personalizations[0].cc = ccEmail.split(',').map(i => ({ email: i.trim() })); + } + + if (headers?.details.length) { + const parsedHeaders: { [key: string]: string } = {}; + headers.details.forEach(obj => parsedHeaders[obj['key']] = obj['value']); + body.headers = parsedHeaders; + } + + if (categories) { + body.categories = categories.split(',') as string[]; + } + + if (ipPoolName) { + body.ip_pool_name = ipPoolName as string; + } + + if (sendAt) { + body.personalizations[0].send_at = moment.tz(sendAt, timezone).unix(); + } + + const data = await sendGridApiRequest.call(this, '/mail/send', 'POST', body, qs, { resolveWithFullResponse: true }); + + returnData.push({ messageId: data!.headers['x-message-id'] }); + } + } + } return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/SentryIo/GenericFunctions.ts b/packages/nodes-base/nodes/SentryIo/GenericFunctions.ts index f318c44ba2..326c49d471 100644 --- a/packages/nodes-base/nodes/SentryIo/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SentryIo/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -24,7 +24,7 @@ export async function sentryIoApiRequest(this: IHookFunctions | IExecuteFunction method, qs, body, - uri: uri ||`https://sentry.io${resource}`, + uri: uri || `https://sentry.io${resource}`, json: true, }; if (!Object.keys(body).length) { @@ -73,7 +73,7 @@ export async function sentryIoApiRequest(this: IHookFunctions | IExecuteFunction } } -export async function sentryApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function sentryApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; @@ -99,17 +99,17 @@ export async function sentryApiRequestAllItems(this: IHookFunctions | IExecuteFu } function getNext(link: string) { - if (link === undefined) { + if (link === undefined) { return; } const next = link.split(',')[1]; if (next.includes('rel="next"')) { - return next.split(';')[0].replace('<', '').replace('>','').trim(); + return next.split(';')[0].replace('<', '').replace('>', '').trim(); } } function hasMore(link: string) { - if (link === undefined) { + if (link === undefined) { return; } const next = link.split(',')[1]; diff --git a/packages/nodes-base/nodes/SentryIo/IssueDescription.ts b/packages/nodes-base/nodes/SentryIo/IssueDescription.ts index 37d9abbe7e..8ac624f7d3 100644 --- a/packages/nodes-base/nodes/SentryIo/IssueDescription.ts +++ b/packages/nodes-base/nodes/SentryIo/IssueDescription.ts @@ -171,6 +171,13 @@ export const issueFields = [ }, }, options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: 'An optional Sentry structured search query. If not provided an implied "is:unresolved" is assumed. Info here.', + }, { displayName: 'Stats Period', name: 'statsPeriod', diff --git a/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts b/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts index 0e02a28522..f545683416 100644 --- a/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts +++ b/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts @@ -56,14 +56,14 @@ export class SentryIo implements INodeType { description: INodeTypeDescription = { displayName: 'Sentry.io', name: 'sentryIo', - icon: 'file:sentryio.png', + icon: 'file:sentryio.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Sentry.io API', defaults: { name: 'Sentry.io', - color: '#000000', + color: '#362d59', }, inputs: ['main'], outputs: ['main'], diff --git a/packages/nodes-base/nodes/SentryIo/sentryio.png b/packages/nodes-base/nodes/SentryIo/sentryio.png deleted file mode 100644 index a44ffc3ccd..0000000000 Binary files a/packages/nodes-base/nodes/SentryIo/sentryio.png and /dev/null differ diff --git a/packages/nodes-base/nodes/SentryIo/sentryio.svg b/packages/nodes-base/nodes/SentryIo/sentryio.svg new file mode 100644 index 0000000000..11d971474b --- /dev/null +++ b/packages/nodes-base/nodes/SentryIo/sentryio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Set.node.ts b/packages/nodes-base/nodes/Set.node.ts index 5e9b93282c..9c17ec7e50 100644 --- a/packages/nodes-base/nodes/Set.node.ts +++ b/packages/nodes-base/nodes/Set.node.ts @@ -38,6 +38,7 @@ export class Set implements INodeType { type: 'fixedCollection', typeOptions: { multipleValues: true, + sortable: true, }, description: 'The value to set.', default: {}, @@ -141,7 +142,7 @@ export class Set implements INodeType { let item: INodeExecutionData; let keepOnlySet: boolean; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, []) as boolean; + keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean; item = items[itemIndex]; const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index d844001b88..132f3b339d 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -741,7 +741,7 @@ export class Slack implements INodeType { } } else { - const attachmentsJson = this.getNodeParameter('attachmentsJson', i, []) as string; + const attachmentsJson = this.getNodeParameter('attachmentsJson', i, '') as string; const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; if (attachmentsJson !== '' && validateJSON(attachmentsJson) === undefined) { throw new Error('Attachments it is not a valid json'); diff --git a/packages/nodes-base/nodes/Slack/slack.svg b/packages/nodes-base/nodes/Slack/slack.svg index a0dd9b585e..a69053160d 100644 --- a/packages/nodes-base/nodes/Slack/slack.svg +++ b/packages/nodes-base/nodes/Slack/slack.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/nodes-base/nodes/Spotify/GenericFunctions.ts b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts index 539b43f1cb..8c91c4957c 100644 --- a/packages/nodes-base/nodes/Spotify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts @@ -1,4 +1,6 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; import { IExecuteFunctions, diff --git a/packages/nodes-base/nodes/Spotify/IsoCountryCodes.ts b/packages/nodes-base/nodes/Spotify/IsoCountryCodes.ts new file mode 100644 index 0000000000..0358d866e1 --- /dev/null +++ b/packages/nodes-base/nodes/Spotify/IsoCountryCodes.ts @@ -0,0 +1,1000 @@ +// `getNewReleases` requires an ISO 3166-1 alpha-2 country code + +export const isoCountryCodes = [ + { + name: 'Afghanistan', + alpha2: 'AF', + }, + { + name: 'Åland Islands', + alpha2: 'AX', + }, + { + name: 'Albania', + alpha2: 'AL', + }, + { + name: 'Algeria', + alpha2: 'DZ', + }, + { + name: 'American Samoa', + alpha2: 'AS', + }, + { + name: 'Andorra', + alpha2: 'AD', + }, + { + name: 'Angola', + alpha2: 'AO', + }, + { + name: 'Anguilla', + alpha2: 'AI', + }, + { + name: 'Antarctica', + alpha2: 'AQ', + }, + { + name: 'Antigua and Barbuda', + alpha2: 'AG', + }, + { + name: 'Argentina', + alpha2: 'AR', + }, + { + name: 'Armenia', + alpha2: 'AM', + }, + { + name: 'Aruba', + alpha2: 'AW', + }, + { + name: 'Australia', + alpha2: 'AU', + }, + { + name: 'Austria', + alpha2: 'AT', + }, + { + name: 'Azerbaijan', + alpha2: 'AZ', + }, + { + name: 'Bahamas (the)', + alpha2: 'BS', + }, + { + name: 'Bahrain', + alpha2: 'BH', + }, + { + name: 'Bangladesh', + alpha2: 'BD', + }, + { + name: 'Barbados', + alpha2: 'BB', + }, + { + name: 'Belarus', + alpha2: 'BY', + }, + { + name: 'Belgium', + alpha2: 'BE', + }, + { + name: 'Belize', + alpha2: 'BZ', + }, + { + name: 'Benin', + alpha2: 'BJ', + }, + { + name: 'Bermuda', + alpha2: 'BM', + }, + { + name: 'Bhutan', + alpha2: 'BT', + }, + { + name: 'Bolivia (Plurinational State of)', + alpha2: 'BO', + }, + { + name: 'Bonaire, Sint Eustatius and Saba', + alpha2: 'BQ', + }, + { + name: 'Bosnia and Herzegovina', + alpha2: 'BA', + }, + { + name: 'Botswana', + alpha2: 'BW', + }, + { + name: 'Bouvet Island', + alpha2: 'BV', + }, + { + name: 'Brazil', + alpha2: 'BR', + }, + { + name: 'British Indian Ocean Territory (the)', + alpha2: 'IO', + }, + { + name: 'Brunei Darussalam', + alpha2: 'BN', + }, + { + name: 'Bulgaria', + alpha2: 'BG', + }, + { + name: 'Burkina Faso', + alpha2: 'BF', + }, + { + name: 'Burundi', + alpha2: 'BI', + }, + { + name: 'Cabo Verde', + alpha2: 'CV', + }, + { + name: 'Cambodia', + alpha2: 'KH', + }, + { + name: 'Cameroon', + alpha2: 'CM', + }, + { + name: 'Canada', + alpha2: 'CA', + }, + { + name: 'Cayman Islands (the)', + alpha2: 'KY', + }, + { + name: 'Central African Republic (the)', + alpha2: 'CF', + }, + { + name: 'Chad', + alpha2: 'TD', + }, + { + name: 'Chile', + alpha2: 'CL', + }, + { + name: 'China', + alpha2: 'CN', + }, + { + name: 'Christmas Island', + alpha2: 'CX', + }, + { + name: 'Cocos (Keeling) Islands (the)', + alpha2: 'CC', + }, + { + name: 'Colombia', + alpha2: 'CO', + }, + { + name: 'Comoros (the)', + alpha2: 'KM', + }, + { + name: 'Congo (the Democratic Republic of the)', + alpha2: 'CD', + }, + { + name: 'Congo (the)', + alpha2: 'CG', + }, + { + name: 'Cook Islands (the)', + alpha2: 'CK', + }, + { + name: 'Costa Rica', + alpha2: 'CR', + }, + { + name: 'Côte d\'Ivoire', + alpha2: 'CI', + }, + { + name: 'Croatia', + alpha2: 'HR', + }, + { + name: 'Cuba', + alpha2: 'CU', + }, + { + name: 'Curaçao', + alpha2: 'CW', + }, + { + name: 'Cyprus', + alpha2: 'CY', + }, + { + name: 'Czechia', + alpha2: 'CZ', + }, + { + name: 'Denmark', + alpha2: 'DK', + }, + { + name: 'Djibouti', + alpha2: 'DJ', + }, + { + name: 'Dominica', + alpha2: 'DM', + }, + { + name: 'Dominican Republic (the)', + alpha2: 'DO', + }, + { + name: 'Ecuador', + alpha2: 'EC', + }, + { + name: 'Egypt', + alpha2: 'EG', + }, + { + name: 'El Salvador', + alpha2: 'SV', + }, + { + name: 'Equatorial Guinea', + alpha2: 'GQ', + }, + { + name: 'Eritrea', + alpha2: 'ER', + }, + { + name: 'Estonia', + alpha2: 'EE', + }, + { + name: 'Ethiopia', + alpha2: 'ET', + }, + { + name: 'Falkland Islands (the) [Malvinas]', + alpha2: 'FK', + }, + { + name: 'Faroe Islands (the)', + alpha2: 'FO', + }, + { + name: 'Fiji', + alpha2: 'FJ', + }, + { + name: 'Finland', + alpha2: 'FI', + }, + { + name: 'France', + alpha2: 'FR', + }, + { + name: 'French Guiana', + alpha2: 'GF', + }, + { + name: 'French Polynesia', + alpha2: 'PF', + }, + { + name: 'French Southern Territories (the)', + alpha2: 'TF', + }, + { + name: 'Gabon', + alpha2: 'GA', + }, + { + name: 'Gambia (the)', + alpha2: 'GM', + }, + { + name: 'Georgia', + alpha2: 'GE', + }, + { + name: 'Germany', + alpha2: 'DE', + }, + { + name: 'Ghana', + alpha2: 'GH', + }, + { + name: 'Gibraltar', + alpha2: 'GI', + }, + { + name: 'Greece', + alpha2: 'GR', + }, + { + name: 'Greenland', + alpha2: 'GL', + }, + { + name: 'Grenada', + alpha2: 'GD', + }, + { + name: 'Guadeloupe', + alpha2: 'GP', + }, + { + name: 'Guam', + alpha2: 'GU', + }, + { + name: 'Guatemala', + alpha2: 'GT', + }, + { + name: 'Guernsey', + alpha2: 'GG', + }, + { + name: 'Guinea', + alpha2: 'GN', + }, + { + name: 'Guinea-Bissau', + alpha2: 'GW', + }, + { + name: 'Guyana', + alpha2: 'GY', + }, + { + name: 'Haiti', + alpha2: 'HT', + }, + { + name: 'Heard Island and McDonald Islands', + alpha2: 'HM', + }, + { + name: 'Holy See (the)', + alpha2: 'VA', + }, + { + name: 'Honduras', + alpha2: 'HN', + }, + { + name: 'Hong Kong', + alpha2: 'HK', + }, + { + name: 'Hungary', + alpha2: 'HU', + }, + { + name: 'Iceland', + alpha2: 'IS', + }, + { + name: 'India', + alpha2: 'IN', + }, + { + name: 'Indonesia', + alpha2: 'ID', + }, + { + name: 'Iran (Islamic Republic of)', + alpha2: 'IR', + }, + { + name: 'Iraq', + alpha2: 'IQ', + }, + { + name: 'Ireland', + alpha2: 'IE', + }, + { + name: 'Isle of Man', + alpha2: 'IM', + }, + { + name: 'Israel', + alpha2: 'IL', + }, + { + name: 'Italy', + alpha2: 'IT', + }, + { + name: 'Jamaica', + alpha2: 'JM', + }, + { + name: 'Japan', + alpha2: 'JP', + }, + { + name: 'Jersey', + alpha2: 'JE', + }, + { + name: 'Jordan', + alpha2: 'JO', + }, + { + name: 'Kazakhstan', + alpha2: 'KZ', + }, + { + name: 'Kenya', + alpha2: 'KE', + }, + { + name: 'Kiribati', + alpha2: 'KI', + }, + { + name: 'Korea (the Democratic People\'s Republic of)', + alpha2: 'KP', + }, + { + name: 'Korea (the Republic of)', + alpha2: 'KR', + }, + { + name: 'Kuwait', + alpha2: 'KW', + }, + { + name: 'Kyrgyzstan', + alpha2: 'KG', + }, + { + name: 'Lao People\'s Democratic Republic (the)', + alpha2: 'LA', + }, + { + name: 'Latvia', + alpha2: 'LV', + }, + { + name: 'Lebanon', + alpha2: 'LB', + }, + { + name: 'Lesotho', + alpha2: 'LS', + }, + { + name: 'Liberia', + alpha2: 'LR', + }, + { + name: 'Libya', + alpha2: 'LY', + }, + { + name: 'Liechtenstein', + alpha2: 'LI', + }, + { + name: 'Lithuania', + alpha2: 'LT', + }, + { + name: 'Luxembourg', + alpha2: 'LU', + }, + { + name: 'Macao', + alpha2: 'MO', + }, + { + name: 'Macedonia (the former Yugoslav Republic of)', + alpha2: 'MK', + }, + { + name: 'Madagascar', + alpha2: 'MG', + }, + { + name: 'Malawi', + alpha2: 'MW', + }, + { + name: 'Malaysia', + alpha2: 'MY', + }, + { + name: 'Maldives', + alpha2: 'MV', + }, + { + name: 'Mali', + alpha2: 'ML', + }, + { + name: 'Malta', + alpha2: 'MT', + }, + { + name: 'Marshall Islands (the)', + alpha2: 'MH', + }, + { + name: 'Martinique', + alpha2: 'MQ', + }, + { + name: 'Mauritania', + alpha2: 'MR', + }, + { + name: 'Mauritius', + alpha2: 'MU', + }, + { + name: 'Mayotte', + alpha2: 'YT', + }, + { + name: 'Mexico', + alpha2: 'MX', + }, + { + name: 'Micronesia (Federated States of)', + alpha2: 'FM', + }, + { + name: 'Moldova (the Republic of)', + alpha2: 'MD', + }, + { + name: 'Monaco', + alpha2: 'MC', + }, + { + name: 'Mongolia', + alpha2: 'MN', + }, + { + name: 'Montenegro', + alpha2: 'ME', + }, + { + name: 'Montserrat', + alpha2: 'MS', + }, + { + name: 'Morocco', + alpha2: 'MA', + }, + { + name: 'Mozambique', + alpha2: 'MZ', + }, + { + name: 'Myanmar', + alpha2: 'MM', + }, + { + name: 'Namibia', + alpha2: 'NA', + }, + { + name: 'Nauru', + alpha2: 'NR', + }, + { + name: 'Nepal', + alpha2: 'NP', + }, + { + name: 'Netherlands (the)', + alpha2: 'NL', + }, + { + name: 'New Caledonia', + alpha2: 'NC', + }, + { + name: 'New Zealand', + alpha2: 'NZ', + }, + { + name: 'Nicaragua', + alpha2: 'NI', + }, + { + name: 'Niger (the)', + alpha2: 'NE', + }, + { + name: 'Nigeria', + alpha2: 'NG', + }, + { + name: 'Niue', + alpha2: 'NU', + }, + { + name: 'Norfolk Island', + alpha2: 'NF', + }, + { + name: 'Northern Mariana Islands (the)', + alpha2: 'MP', + }, + { + name: 'Norway', + alpha2: 'NO', + }, + { + name: 'Oman', + alpha2: 'OM', + }, + { + name: 'Pakistan', + alpha2: 'PK', + }, + { + name: 'Palau', + alpha2: 'PW', + }, + { + name: 'Palestine, State of', + alpha2: 'PS', + }, + { + name: 'Panama', + alpha2: 'PA', + }, + { + name: 'Papua New Guinea', + alpha2: 'PG', + }, + { + name: 'Paraguay', + alpha2: 'PY', + }, + { + name: 'Peru', + alpha2: 'PE', + }, + { + name: 'Philippines (the)', + alpha2: 'PH', + }, + { + name: 'Pitcairn', + alpha2: 'PN', + }, + { + name: 'Poland', + alpha2: 'PL', + }, + { + name: 'Portugal', + alpha2: 'PT', + }, + { + name: 'Puerto Rico', + alpha2: 'PR', + }, + { + name: 'Qatar', + alpha2: 'QA', + }, + { + name: 'Réunion', + alpha2: 'RE', + }, + { + name: 'Romania', + alpha2: 'RO', + }, + { + name: 'Russian Federation (the)', + alpha2: 'RU', + }, + { + name: 'Rwanda', + alpha2: 'RW', + }, + { + name: 'Saint Barthélemy', + alpha2: 'BL', + }, + { + name: 'Saint Helena, Ascension and Tristan da Cunha', + alpha2: 'SH', + }, + { + name: 'Saint Kitts and Nevis', + alpha2: 'KN', + }, + { + name: 'Saint Lucia', + alpha2: 'LC', + }, + { + name: 'Saint Martin (French part)', + alpha2: 'MF', + }, + { + name: 'Saint Pierre and Miquelon', + alpha2: 'PM', + }, + { + name: 'Saint Vincent and the Grenadines', + alpha2: 'VC', + }, + { + name: 'Samoa', + alpha2: 'WS', + }, + { + name: 'San Marino', + alpha2: 'SM', + }, + { + name: 'Sao Tome and Principe', + alpha2: 'ST', + }, + { + name: 'Saudi Arabia', + alpha2: 'SA', + }, + { + name: 'Senegal', + alpha2: 'SN', + }, + { + name: 'Serbia', + alpha2: 'RS', + }, + { + name: 'Seychelles', + alpha2: 'SC', + }, + { + name: 'Sierra Leone', + alpha2: 'SL', + }, + { + name: 'Singapore', + alpha2: 'SG', + }, + { + name: 'Sint Maarten (Dutch part)', + alpha2: 'SX', + }, + { + name: 'Slovakia', + alpha2: 'SK', + }, + { + name: 'Slovenia', + alpha2: 'SI', + }, + { + name: 'Solomon Islands', + alpha2: 'SB', + }, + { + name: 'Somalia', + alpha2: 'SO', + }, + { + name: 'South Africa', + alpha2: 'ZA', + }, + { + name: 'South Georgia and the South Sandwich Islands', + alpha2: 'GS', + }, + { + name: 'South Sudan', + alpha2: 'SS', + }, + { + name: 'Spain', + alpha2: 'ES', + }, + { + name: 'Sri Lanka', + alpha2: 'LK', + }, + { + name: 'Sudan (the)', + alpha2: 'SD', + }, + { + name: 'Suriname', + alpha2: 'SR', + }, + { + name: 'Svalbard and Jan Mayen', + alpha2: 'SJ', + }, + { + name: 'Swaziland', + alpha2: 'SZ', + }, + { + name: 'Sweden', + alpha2: 'SE', + }, + { + name: 'Switzerland', + alpha2: 'CH', + }, + { + name: 'Syrian Arab Republic', + alpha2: 'SY', + }, + { + name: 'Taiwan (Province of China)', + alpha2: 'TW', + }, + { + name: 'Tajikistan', + alpha2: 'TJ', + }, + { + name: 'Tanzania, United Republic of', + alpha2: 'TZ', + }, + { + name: 'Thailand', + alpha2: 'TH', + }, + { + name: 'Timor-Leste', + alpha2: 'TL', + }, + { + name: 'Togo', + alpha2: 'TG', + }, + { + name: 'Tokelau', + alpha2: 'TK', + }, + { + name: 'Tonga', + alpha2: 'TO', + }, + { + name: 'Trinidad and Tobago', + alpha2: 'TT', + }, + { + name: 'Tunisia', + alpha2: 'TN', + }, + { + name: 'Turkey', + alpha2: 'TR', + }, + { + name: 'Turkmenistan', + alpha2: 'TM', + }, + { + name: 'Turks and Caicos Islands (the)', + alpha2: 'TC', + }, + { + name: 'Tuvalu', + alpha2: 'TV', + }, + { + name: 'Uganda', + alpha2: 'UG', + }, + { + name: 'Ukraine', + alpha2: 'UA', + }, + { + name: 'United Arab Emirates (the)', + alpha2: 'AE', + }, + { + name: 'United Kingdom of Great Britain and Northern Ireland (the)', + alpha2: 'GB', + }, + { + name: 'United States Minor Outlying Islands (the)', + alpha2: 'UM', + }, + { + name: 'United States of America (the)', + alpha2: 'US', + }, + { + name: 'Uruguay', + alpha2: 'UY', + }, + { + name: 'Uzbekistan', + alpha2: 'UZ', + }, + { + name: 'Vanuatu', + alpha2: 'VU', + }, + { + name: 'Venezuela (Bolivarian Republic of)', + alpha2: 'VE', + }, + { + name: 'Viet Nam', + alpha2: 'VN', + }, + { + name: 'Virgin Islands (British)', + alpha2: 'VG', + }, + { + name: 'Virgin Islands (U.S.)', + alpha2: 'VI', + }, + { + name: 'Wallis and Futuna', + alpha2: 'WF', + }, + { + name: 'Western Sahara*', + alpha2: 'EH', + }, + { + name: 'Yemen', + alpha2: 'YE', + }, + { + name: 'Zambia', + alpha2: 'ZM', + }, + { + name: 'Zimbabwe', + alpha2: 'ZW', + }, +]; diff --git a/packages/nodes-base/nodes/Spotify/Spotify.node.ts b/packages/nodes-base/nodes/Spotify/Spotify.node.ts index 662d5d8d4d..762713bd2a 100644 --- a/packages/nodes-base/nodes/Spotify/Spotify.node.ts +++ b/packages/nodes-base/nodes/Spotify/Spotify.node.ts @@ -14,11 +14,15 @@ import { spotifyApiRequestAllItems, } from './GenericFunctions'; +import { + isoCountryCodes +} from './IsoCountryCodes'; + export class Spotify implements INodeType { description: INodeTypeDescription = { displayName: 'Spotify', name: 'spotify', - icon: 'file:spotify.png', + icon: 'file:spotify.svg', group: ['input'], version: 1, description: 'Access public song data via the Spotify API.', @@ -53,6 +57,10 @@ export class Spotify implements INodeType { name: 'Artist', value: 'artist', }, + { + name: 'Library', + value: 'library', + }, { name: 'Player', value: 'player', @@ -183,6 +191,11 @@ export class Spotify implements INodeType { value: 'get', description: 'Get an album by URI or ID.', }, + { + name: 'Get New Releases', + value: 'getNewReleases', + description: 'Get a list of new album releases.', + }, { name: `Get Tracks`, value: 'getTracks', @@ -203,6 +216,10 @@ export class Spotify implements INodeType { resource: [ 'album', ], + operation: [ + 'get', + 'getTracks', + ], }, }, placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', @@ -304,6 +321,11 @@ export class Spotify implements INodeType { value: 'add', description: 'Add tracks from a playlist by track and playlist URI or ID.', }, + { + name: 'Create a Playlist', + value: 'create', + description: 'Create a new playlist.', + }, { name: 'Get', value: 'get', @@ -350,6 +372,59 @@ export class Spotify implements INodeType { placeholder: 'spotify:playlist:37i9dQZF1DWUhI3iC1khPH', description: `The playlist's Spotify URI or its ID.`, }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'playlist', + ], + operation: [ + 'create', + ], + }, + }, + placeholder: 'Favorite Songs', + description: 'Name of the playlist to create.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'playlist', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + placeholder: 'These are all my favorite songs.', + description: 'Description for the playlist to create.', + }, + { + displayName: 'Public', + name: 'public', + type: 'boolean', + default: true, + description: 'Whether the playlist is publicly accessible.', + }, + ], + }, { displayName: 'Track ID', name: 'trackID', @@ -416,6 +491,30 @@ export class Spotify implements INodeType { placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', description: `The track's Spotify URI or ID.`, }, + // -------------------------------------------------------------------------------------------------------- + // Library Operations + // Get liked tracks + // -------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'library', + ], + }, + }, + options: [ + { + name: 'Get Liked Tracks', + value: 'getLikedTracks', + description: `Get the user's liked tracks.`, + }, + ], + default: 'getLikedTracks', + }, { displayName: 'Return All', name: 'returnAll', @@ -427,12 +526,15 @@ export class Spotify implements INodeType { resource: [ 'album', 'artist', + 'library', 'playlist', ], operation: [ 'getTracks', 'getAlbums', 'getUserPlaylists', + 'getNewReleases', + 'getLikedTracks', ], }, }, @@ -449,12 +551,15 @@ export class Spotify implements INodeType { resource: [ 'album', 'artist', + 'library', 'playlist', ], operation: [ 'getTracks', 'getAlbums', 'getUserPlaylists', + 'getNewReleases', + 'getLikedTracks', ], returnAll: [ false, @@ -489,6 +594,33 @@ export class Spotify implements INodeType { }, description: `The number of items to return.`, }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'album', + ], + operation: [ + 'getNewReleases', + ], + }, + }, + options: [ + { + displayName: 'Country', + name: 'country', + type: 'options', + default: 'US', + options: isoCountryCodes.map(({ name, alpha2 }) => ({ name, value: alpha2 })), + description: 'Country to filter new releases by.', + }, + ], + }, ], }; @@ -519,12 +651,14 @@ export class Spotify implements INodeType { qs = {}; returnAll = false; - for(let i = 0; i < items.length; i++) { - // ----------------------------- - // Player Operations - // ----------------------------- - if( resource === 'player' ) { - if(operation === 'pause') { + for (let i = 0; i < items.length; i++) { + if (resource === 'player') { + + // ----------------------------- + // Player Operations + // ----------------------------- + + if (operation === 'pause') { requestMethod = 'PUT'; endpoint = `/me/player/pause`; @@ -533,7 +667,7 @@ export class Spotify implements INodeType { responseData = { success: true }; - } else if(operation === 'recentlyPlayed') { + } else if (operation === 'recentlyPlayed') { requestMethod = 'GET'; endpoint = `/me/player/recently-played`; @@ -548,14 +682,14 @@ export class Spotify implements INodeType { responseData = responseData.items; - } else if(operation === 'currentlyPlaying') { + } else if (operation === 'currentlyPlaying') { requestMethod = 'GET'; endpoint = `/me/player/currently-playing`; responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); - } else if(operation === 'nextSong') { + } else if (operation === 'nextSong') { requestMethod = 'POST'; endpoint = `/me/player/next`; @@ -564,7 +698,7 @@ export class Spotify implements INodeType { responseData = { success: true }; - } else if(operation === 'previousSong') { + } else if (operation === 'previousSong') { requestMethod = 'POST'; endpoint = `/me/player/previous`; @@ -573,7 +707,7 @@ export class Spotify implements INodeType { responseData = { success: true }; - } else if(operation === 'startMusic') { + } else if (operation === 'startMusic') { requestMethod = 'PUT'; endpoint = `/me/player/play`; @@ -586,7 +720,7 @@ export class Spotify implements INodeType { responseData = { success: true }; - } else if(operation === 'addSongToQueue') { + } else if (operation === 'addSongToQueue') { requestMethod = 'POST'; endpoint = `/me/player/queue`; @@ -601,22 +735,50 @@ export class Spotify implements INodeType { responseData = { success: true }; } - // ----------------------------- - // Album Operations - // ----------------------------- - } else if( resource === 'album') { - const uri = this.getNodeParameter('id', i) as string; - const id = uri.replace('spotify:album:', ''); + } else if (resource === 'album') { - requestMethod = 'GET'; + // ----------------------------- + // Album Operations + // ----------------------------- + + if (operation === 'get') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:album:', ''); + + requestMethod = 'GET'; - if(operation === 'get') { endpoint = `/albums/${id}`; responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); - } else if(operation === 'getTracks') { + } else if (operation === 'getNewReleases') { + + endpoint = '/browse/new-releases'; + requestMethod = 'GET'; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (Object.keys(filters).length) { + Object.assign(qs, filters); + } + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i); + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.albums.items; + } + + } else if (operation === 'getTracks') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:album:', ''); + + requestMethod = 'GET'; + endpoint = `/albums/${id}/tracks`; propertyName = 'tracks'; @@ -625,7 +787,7 @@ export class Spotify implements INodeType { propertyName = 'items'; - if(!returnAll) { + if (!returnAll) { const limit = this.getNodeParameter('limit', i) as number; qs = { @@ -637,15 +799,18 @@ export class Spotify implements INodeType { responseData = responseData.items; } } - // ----------------------------- - // Artist Operations - // ----------------------------- - } else if( resource === 'artist') { + + } else if (resource === 'artist') { + + // ----------------------------- + // Artist Operations + // ----------------------------- + const uri = this.getNodeParameter('id', i) as string; const id = uri.replace('spotify:artist:', ''); - if(operation === 'getAlbums') { + if (operation === 'getAlbums') { endpoint = `/artists/${id}/albums`; @@ -653,7 +818,7 @@ export class Spotify implements INodeType { propertyName = 'items'; - if(!returnAll) { + if (!returnAll) { const limit = this.getNodeParameter('limit', i) as number; qs = { @@ -664,7 +829,8 @@ export class Spotify implements INodeType { responseData = responseData.items; } - } else if(operation === 'getRelatedArtists') { + + } else if (operation === 'getRelatedArtists') { endpoint = `/artists/${id}/related-artists`; @@ -672,7 +838,7 @@ export class Spotify implements INodeType { responseData = responseData.artists; - } else if(operation === 'getTopTracks'){ + } else if (operation === 'getTopTracks') { const country = this.getNodeParameter('country', i) as string; qs = { @@ -693,23 +859,25 @@ export class Spotify implements INodeType { responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); } - // ----------------------------- - // Playlist Operations - // ----------------------------- - } else if( resource === 'playlist') { - if(['delete', 'get', 'getTracks', 'add'].includes(operation)) { + + } else if (resource === 'playlist') { + + // ----------------------------- + // Playlist Operations + // ----------------------------- + + if (['delete', 'get', 'getTracks', 'add'].includes(operation)) { const uri = this.getNodeParameter('id', i) as string; const id = uri.replace('spotify:playlist:', ''); - if(operation === 'delete') { + if (operation === 'delete') { requestMethod = 'DELETE'; const trackId = this.getNodeParameter('trackID', i) as string; body.tracks = [ { - uri: `${trackId}`, - positions: [ 0 ], + uri: trackId, }, ]; @@ -719,14 +887,14 @@ export class Spotify implements INodeType { responseData = { success: true }; - } else if(operation === 'get') { + } else if (operation === 'get') { requestMethod = 'GET'; endpoint = `/playlists/${id}`; responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); - } else if(operation === 'getTracks') { + } else if (operation === 'getTracks') { requestMethod = 'GET'; endpoint = `/playlists/${id}/tracks`; @@ -735,7 +903,7 @@ export class Spotify implements INodeType { propertyName = 'items'; - if(!returnAll) { + if (!returnAll) { const limit = this.getNodeParameter('limit', i) as number; qs = { @@ -746,7 +914,7 @@ export class Spotify implements INodeType { responseData = responseData.items; } - } else if(operation === 'add') { + } else if (operation === 'add') { requestMethod = 'POST'; const trackId = this.getNodeParameter('trackID', i) as string; @@ -760,47 +928,92 @@ export class Spotify implements INodeType { responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); } - } else if(operation === 'getUserPlaylists') { - requestMethod = 'GET'; + } else if (operation === 'getUserPlaylists') { + requestMethod = 'GET'; - endpoint = '/me/playlists'; + endpoint = '/me/playlists'; - returnAll = this.getNodeParameter('returnAll', i) as boolean; + returnAll = this.getNodeParameter('returnAll', i) as boolean; - propertyName = 'items'; + propertyName = 'items'; - if(!returnAll) { - const limit = this.getNodeParameter('limit', i) as number; + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; - qs = { - limit, - }; + qs = { + limit, + }; - responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); - responseData = responseData.items; - } + responseData = responseData.items; } - // ----------------------------- - // Track Operations - // ----------------------------- - } else if( resource === 'track') { + + } else if (operation === 'create') { + + // https://developer.spotify.com/console/post-playlists/ + + body.name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, additionalFields); + } + + responseData = await spotifyApiRequest.call(this, 'POST', '/me/playlists', body, qs); + } + + } else if (resource === 'track') { + + // ----------------------------- + // Track Operations + // ----------------------------- + const uri = this.getNodeParameter('id', i) as string; const id = uri.replace('spotify:track:', ''); requestMethod = 'GET'; - if(operation === 'getAudioFeatures') { + if (operation === 'getAudioFeatures') { endpoint = `/audio-features/${id}`; - } else if(operation === 'get') { + } else if (operation === 'get') { endpoint = `/tracks/${id}`; } responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if (resource === 'library') { + + // ----------------------------- + // Library Operations + // ----------------------------- + + if (operation === 'getLikedTracks') { + requestMethod = 'GET'; + + endpoint = '/me/tracks'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + limit, + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } } - if(returnAll) { + if (returnAll) { responseData = await spotifyApiRequestAllItems.call(this, propertyName, requestMethod, endpoint, body, qs); } diff --git a/packages/nodes-base/nodes/Spotify/spotify.png b/packages/nodes-base/nodes/Spotify/spotify.png deleted file mode 100644 index 14d3dea4ae..0000000000 Binary files a/packages/nodes-base/nodes/Spotify/spotify.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Spotify/spotify.svg b/packages/nodes-base/nodes/Spotify/spotify.svg new file mode 100644 index 0000000000..76fd7c810f --- /dev/null +++ b/packages/nodes-base/nodes/Spotify/spotify.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/Stackby/Stackby.node.json b/packages/nodes-base/nodes/Stackby/Stackby.node.json new file mode 100644 index 0000000000..0802db76e6 --- /dev/null +++ b/packages/nodes-base/nodes/Stackby/Stackby.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.stackby", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Data & Storage" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/stackby" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.stackby/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Strava/GenericFunctions.ts b/packages/nodes-base/nodes/Strava/GenericFunctions.ts index 832ee3e652..8f21ac7f82 100644 --- a/packages/nodes-base/nodes/Strava/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Strava/GenericFunctions.ts @@ -45,7 +45,7 @@ export async function stravaApiRequest(this: IExecuteFunctions | IExecuteSingleF } else { //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'stravaOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'stravaOAuth2Api', options, { includeCredentialsOnRefreshOnBody: true }); } } catch (error) { diff --git a/packages/nodes-base/nodes/Switch.node.ts b/packages/nodes-base/nodes/Switch.node.ts index 3d18abf16a..fe6213eb0d 100644 --- a/packages/nodes-base/nodes/Switch.node.ts +++ b/packages/nodes-base/nodes/Switch.node.ts @@ -87,6 +87,10 @@ export class Switch implements INodeType { name: 'Boolean', value: 'boolean', }, + { + name: 'Date & Time', + value: 'dateTime', + }, { name: 'Number', value: 'number', @@ -185,6 +189,91 @@ export class Switch implements INodeType { ], }, + // ---------------------------------- + // dataType:dateTime + // ---------------------------------- + { + displayName: 'Value 1', + name: 'value1', + type: 'dateTime', + displayOptions: { + show: { + dataType: [ + 'dateTime', + ], + mode: [ + 'rules', + ], + }, + }, + default: '', + description: 'The value to compare with the second one.', + }, + { + displayName: 'Routing Rules', + name: 'rules', + placeholder: 'Add Routing Rule', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataType: [ + 'dateTime', + ], + mode: [ + 'rules', + ], + }, + }, + description: 'The routing rules.', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Dates', + values: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Occurred after', + value: 'after', + }, + { + name: 'Occurred before', + value: 'before', + }, + ], + default: 'after', + description: 'Operation to decide where the the data should be mapped to.', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'dateTime', + default: 0, + description: 'The value to compare with the first one.', + }, + { + displayName: 'Output', + name: 'output', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 3, + }, + default: 0, + description: 'The index of output to which to send data to if rule matches.', + }, + ], + }, + ], + }, + // ---------------------------------- // dataType:number // ---------------------------------- @@ -470,12 +559,14 @@ export class Switch implements INodeType { let mode: string; let outputIndex: number; let ruleData: INodeParameters; - let value1: NodeParameterValue; + let value1: NodeParameterValue, value2: NodeParameterValue; // The compare operations const compareOperationFunctions: { [key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean; } = { + after: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0), + before: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0), contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()), notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()), endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string), @@ -502,12 +593,32 @@ export class Switch implements INodeType { }, }; + // Converts the input data of a dateTime into a number for easy compare + function convertDateTime(value: NodeParameterValue): number { + let returnValue: number | undefined = undefined; + if (typeof value === 'string') { + returnValue = new Date(value).getTime(); + } else if (typeof value === 'number') { + returnValue = value; + } if ((value as unknown as object) instanceof Date) { + returnValue = (value as unknown as Date).getTime(); + } + + if (returnValue === undefined || isNaN(returnValue)) { + throw new Error(`The value "${value}" is not a valid DateTime.`); + } + + return returnValue; + } + function checkIndexRange(index: number) { if (index < 0 || index >= returnData.length) { throw new Error(`The ouput ${index} is not allowed. It has to be between 0 and ${returnData.length - 1}!`); } } + const dataType = this.getNodeParameter('dataType', 0) as string; + // Itterate over all items to check to which output they should be routed to itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { @@ -525,10 +636,19 @@ export class Switch implements INodeType { // Rules decide how to route item value1 = this.getNodeParameter('value1', itemIndex) as NodeParameterValue; + if (dataType === 'dateTime') { + value1 = convertDateTime(value1); + } for (ruleData of this.getNodeParameter('rules.rules', itemIndex, []) as INodeParameters[]) { // Check if the values passes - compareOperationResult = compareOperationFunctions[ruleData.operation as string](value1, ruleData.value2 as NodeParameterValue); + + value2 = ruleData.value2 as NodeParameterValue; + if (dataType === 'dateTime') { + value2 = convertDateTime(value2); + } + + compareOperationResult = compareOperationFunctions[ruleData.operation as string](value1, value2); if (compareOperationResult === true) { // If rule matches add it to the correct output and continue with next item diff --git a/packages/nodes-base/nodes/Tapfiliate/Tapfiliate.node.json b/packages/nodes-base/nodes/Tapfiliate/Tapfiliate.node.json new file mode 100644 index 0000000000..4a29b2b539 --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/Tapfiliate.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.tapfiliate", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Sales" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/tapfiliate" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.tapfiliate/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index ea9b7afb8e..e22d791c70 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -156,11 +156,15 @@ export class Telegram implements INodeType { value: 'answerQuery', description: 'Send answer to callback query sent from inline keyboard.', }, + { + name: 'Answer Inline Query', + value: 'answerInlineQuery', + description: 'Send answer to callback query sent from inline bot.', + }, ], default: 'answerQuery', description: 'The operation to perform.', }, - { displayName: 'Operation', name: 'operation', @@ -230,6 +234,11 @@ export class Telegram implements INodeType { value: 'sendDocument', description: 'Send a document', }, + { + name: 'Send Location', + value: 'sendLocation', + description: 'Send a location', + }, { name: 'Send Message', value: 'sendMessage', @@ -284,6 +293,7 @@ export class Telegram implements INodeType { 'sendAudio', 'sendChatAction', 'sendDocument', + 'sendLocation', 'sendMessage', 'sendMediaGroup', 'sendPhoto', @@ -502,13 +512,104 @@ export class Telegram implements INodeType { ], }, + // ----------------------------------------------- + // callback:answerInlineQuery + // ----------------------------------------------- + { + displayName: 'Query ID', + name: 'queryId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'answerInlineQuery', + ], + resource: [ + 'callback', + ], + }, + }, + required: true, + description: 'Unique identifier for the answered query.', + }, + { + displayName: 'Results', + name: 'results', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'answerInlineQuery', + ], + resource: [ + 'callback', + ], + }, + }, + required: true, + description: 'A JSON-serialized array of results for the inline query.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'answerInlineQuery', + ], + resource: [ + 'callback', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Cache Time', + name: 'cache_time', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'The maximum amount of time in seconds that the result of the callback query may be cached client-side.', + }, + { + displayName: 'Show Alert', + name: 'show_alert', + type: 'boolean', + default: false, + description: 'If true, an alert will be shown by the client instead of a notification at the top of the chat screen.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'URL that will be opened by the user\'s client.', + }, + ], + }, // ---------------------------------- // file // ---------------------------------- - // ---------------------------------- // file:get/download // ---------------------------------- @@ -811,6 +912,55 @@ export class Telegram implements INodeType { }, + // ---------------------------------- + // message:sendLocation + // ---------------------------------- + { + displayName: 'Latitude', + name: 'latitude', + type: 'number', + default: 0.0, + typeOptions: { + numberPrecision: 10, + minValue: -90, + maxValue: 90, + }, + displayOptions: { + show: { + operation: [ + 'sendLocation', + ], + resource: [ + 'message', + ], + }, + }, + description: 'Location latitude', + }, + + { + displayName: 'Longitude', + name: 'longitude', + type: 'number', + typeOptions: { + numberPrecision: 10, + minValue: -180, + maxValue: 180, + }, + default: 0.0, + displayOptions: { + show: { + operation: [ + 'sendLocation', + ], + resource: [ + 'message', + ], + }, + }, + description: 'Location longitude', + }, + // ---------------------------------- // message:sendMediaGroup // ---------------------------------- @@ -999,7 +1149,7 @@ export class Telegram implements INodeType { // ---------------------------------- - // message:editMessageText/sendAnimation/sendAudio/sendMessage/sendPhoto/sendSticker/sendVideo + // message:editMessageText/sendAnimation/sendAudio/sendLocation/sendMessage/sendPhoto/sendSticker/sendVideo // ---------------------------------- { @@ -1015,6 +1165,7 @@ export class Telegram implements INodeType { 'sendSticker', 'sendVideo', 'sendAudio', + 'sendLocation', ], resource: [ 'message', @@ -1340,6 +1491,7 @@ export class Telegram implements INodeType { 'sendAnimation', 'sendAudio', 'sendDocument', + 'sendLocation', 'sendMessage', 'sendMediaGroup', 'sendPhoto', @@ -1592,7 +1744,21 @@ export class Telegram implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; Object.assign(body, additionalFields); + } else if (operation === 'answerInlineQuery') { + // ----------------------------------------------- + // callback:answerInlineQuery + // ----------------------------------------------- + + endpoint = 'answerInlineQuery'; + + body.inline_query_id = this.getNodeParameter('queryId', i) as string; + body.results = this.getNodeParameter('results', i) as string; + + // Add additional fields + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(body, additionalFields); } + } else if (resource === 'chat') { if (operation === 'get') { // ---------------------------------- @@ -1757,6 +1923,20 @@ export class Telegram implements INodeType { // Add additional fields and replyMarkup addAdditionalFields.call(this, body, i); + } else if (operation === 'sendLocation') { + // ---------------------------------- + // message:sendLocation + // ---------------------------------- + + endpoint = 'sendLocation'; + + body.chat_id = this.getNodeParameter('chatId', i) as string; + body.latitude = this.getNodeParameter('latitude', i) as string; + body.longitude = this.getNodeParameter('longitude', i) as string; + + // Add additional fields and replyMarkup + addAdditionalFields.call(this, body, i); + } else if (operation === 'sendMessage') { // ---------------------------------- // message:sendMessage diff --git a/packages/nodes-base/nodes/Telegram/telegram.svg b/packages/nodes-base/nodes/Telegram/telegram.svg index 8256014e98..fe13cb81f4 100644 --- a/packages/nodes-base/nodes/Telegram/telegram.svg +++ b/packages/nodes-base/nodes/Telegram/telegram.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts index 3e3e9ae4a3..4a64dd36bb 100644 --- a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts @@ -29,6 +29,7 @@ export async function theHiveApiRequest(this: IHookFunctions | IExecuteFunctions qs: query, uri: uri || `${credentials.url}/api${resource}`, body, + rejectUnauthorized: credentials.allowUnauthorizedCerts as boolean, json: true, }; diff --git a/packages/nodes-base/nodes/TheHive/TheHive.node.ts b/packages/nodes-base/nodes/TheHive/TheHive.node.ts index 72155b8044..dc40862a62 100644 --- a/packages/nodes-base/nodes/TheHive/TheHive.node.ts +++ b/packages/nodes-base/nodes/TheHive/TheHive.node.ts @@ -67,7 +67,7 @@ export class TheHive implements INodeType { description: INodeTypeDescription = { displayName: 'TheHive', name: 'theHive', - icon: 'file:thehive.png', + icon: 'file:thehive.svg', group: ['transform'], subtitle: '={{$parameter["operation"]}} : {{$parameter["resource"]}}', version: 1, @@ -195,6 +195,50 @@ export class TheHive implements INodeType { ]; return options; }, + async loadObservableTypes(this: ILoadOptionsFunctions): Promise { + const version = this.getCredentials('theHiveApi')?.apiVersion; + const endpoint = version === 'v1' ? '/observable/type?range=all' : '/list/list_artifactDataType'; + + const dataTypes = await theHiveApiRequest.call( + this, + 'GET', + endpoint as string, + ); + + let returnData: INodePropertyOptions[] = []; + + if (version === 'v1') { + returnData = dataTypes.map((dataType: IDataObject) => { + return { + name: dataType.name as string, + value: dataType.name as string, + }; + }); + } + else { + returnData = Object.keys(dataTypes).map(key => { + const dataType = dataTypes[key] as string; + + return { + name: dataType, + value: dataType, + }; + }); + } + + // Sort the array by option name + returnData.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + + return returnData; + }, async loadTaskOptions(this: ILoadOptionsFunctions): Promise { const version = this.getCredentials('theHiveApi')?.apiVersion; const options = [ @@ -216,6 +260,8 @@ export class TheHive implements INodeType { { name: 'Execute Responder', value: 'executeResponder', description: 'Execute a responder on the specified alert' }, { name: 'Get', value: 'get', description: 'Get an alert' }, { name: 'Get All', value: 'getAll', description: 'Get all alerts' }, + { name: 'Mark as Read', value: 'markAsRead', description: 'Mark the alert as read' }, + { name: 'Mark as Unread', value: 'markAsUnread', description: 'Mark the alert as unread' }, { name: 'Merge', value: 'merge', description: 'Merge alert into an existing case' }, { name: 'Promote', value: 'promote', description: 'Promote an alert into a case' }, { name: 'Update', value: 'update', description: 'Update alert' }, @@ -535,6 +581,26 @@ export class TheHive implements INodeType { ); } + if (operation === 'markAsRead') { + const alertId = this.getNodeParameter('id', i) as string; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/alert/${alertId}/markAsRead`, + ); + } + + if (operation === 'markAsUnread') { + const alertId = this.getNodeParameter('id', i) as string; + + responseData = await theHiveApiRequest.call( + this, + 'POST', + `/alert/${alertId}/markAsUnread`, + ); + } + if (operation === 'merge') { const alertId = this.getNodeParameter('id', i) as string; diff --git a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts index 7618bab4e2..fcd211d12e 100644 --- a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts +++ b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts @@ -14,7 +14,7 @@ export class TheHiveTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'TheHive Trigger', name: 'theHiveTrigger', - icon: 'file:thehive.png', + icon: 'file:thehive.svg', group: ['trigger'], version: 1, description: 'Starts the workflow when a TheHive event occurs.', diff --git a/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts index 77ff3336c0..1c607305eb 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts @@ -82,6 +82,8 @@ export const alertFields = [ ], operation: [ 'promote', + 'markAsRead', + 'markAsUnread', 'merge', 'update', 'executeResponder', @@ -385,21 +387,10 @@ export const alertFields = [ name: 'dataType', type: 'options', default: '', - options: [ - { - name: 'IP', - value: 'ip', - }, - { - name: 'Domain', - value: 'domain', - }, - { - name: 'File', - value: 'file', - }, - ], - description: '', + typeOptions: { + loadOptionsMethod: 'loadObservableTypes', + }, + description: 'Type of the observable', }, { displayName: 'Data', @@ -543,20 +534,10 @@ export const alertFields = [ name: 'dataType', type: 'options', default: '', - options: [ - { - name: 'IP', - value: 'ip', - }, - { - name: 'Domain', - value: 'domain', - }, - { - name: 'File', - value: 'file', - }, - ], + typeOptions: { + loadOptionsMethod: 'loadObservableTypes', + }, + description: 'Type of the observable', }, { displayName: 'Data', diff --git a/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts index 4ce4c5f051..1ff1aba4a6 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/CaseDescription.ts @@ -422,12 +422,12 @@ export const caseFields = [ name: 'Indeterminate', }, { - value: 'False Positive', - name: 'FalsePositive', + value: 'FalsePositive', + name: 'False Positive', }, { - value: 'True Positive', - name: 'TruePositive', + value: 'TruePositive', + name: 'True Positive', }, { value: 'Other', diff --git a/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts index 12aa6e9478..9c2386569e 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts @@ -120,64 +120,9 @@ export const observableFields = [ type: 'options', required: true, default: '', - options: [ - { - name: 'domain', - value: 'domain', - }, - { - name: 'file', - value: 'file', - }, - { - name: 'filename', - value: 'filename', - }, - { - name: 'fqdn', - value: 'fqdn', - }, - { - name: 'hash', - value: 'hash', - }, - { - name: 'ip', - value: 'ip', - }, - { - name: 'mail', - value: 'mail', - }, - { - name: 'mail_subject', - value: 'mail_subject', - }, - { - name: 'other', - value: 'other', - }, - { - name: 'regexp', - value: 'regexp', - }, - { - name: 'registry', - value: 'registry', - }, - { - name: 'uri_path', - value: 'uri_path', - }, - { - name: 'url', - value: 'url', - }, - { - name: 'user-agent', - value: 'user-agent', - }, - ], + typeOptions: { + loadOptionsMethod: 'loadObservableTypes', + }, displayOptions: { show: { resource: [ @@ -604,64 +549,9 @@ export const observableFields = [ name: 'dataType', type: 'multiOptions', default: [], - options: [ - { - name: 'domain', - value: 'domain', - }, - { - name: 'file', - value: 'file', - }, - { - name: 'filename', - value: 'filename', - }, - { - name: 'fqdn', - value: 'fqdn', - }, - { - name: 'hash', - value: 'hash', - }, - { - name: 'ip', - value: 'ip', - }, - { - name: 'mail', - value: 'mail', - }, - { - name: 'mail_subject', - value: 'mail_subject', - }, - { - name: 'other', - value: 'other', - }, - { - name: 'regexp', - value: 'regexp', - }, - { - name: 'registry', - value: 'registry', - }, - { - name: 'uri_path', - value: 'uri_path', - }, - { - name: 'url', - value: 'url', - }, - { - name: 'user-agent', - value: 'user-agent', - }, - ], + typeOptions: { + loadOptionsMethod: 'loadObservableTypes', + }, description: 'Type of the observable', }, { diff --git a/packages/nodes-base/nodes/TheHive/thehive.png b/packages/nodes-base/nodes/TheHive/thehive.png deleted file mode 100644 index a28fa90b56..0000000000 Binary files a/packages/nodes-base/nodes/TheHive/thehive.png and /dev/null differ diff --git a/packages/nodes-base/nodes/TheHive/thehive.svg b/packages/nodes-base/nodes/TheHive/thehive.svg new file mode 100644 index 0000000000..25ee988a32 --- /dev/null +++ b/packages/nodes-base/nodes/TheHive/thehive.svg @@ -0,0 +1 @@ +th-logo \ No newline at end of file diff --git a/packages/nodes-base/nodes/Trello/ChecklistDescription.ts b/packages/nodes-base/nodes/Trello/ChecklistDescription.ts index 4079f53c56..c0e65077af 100644 --- a/packages/nodes-base/nodes/Trello/ChecklistDescription.ts +++ b/packages/nodes-base/nodes/Trello/ChecklistDescription.ts @@ -18,6 +18,11 @@ export const checklistOperations = [ }, }, options: [ + { + name: 'Create Checklist Item', + value: 'createCheckItem', + description: 'Create a checklist item', + }, { name: 'Create', value: 'create', @@ -276,6 +281,79 @@ export const checklistFields = [ ], }, + // ---------------------------------- + // checklist:createCheckItem + // ---------------------------------- + { + displayName: 'Checklist ID', + name: 'checklistId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'createCheckItem', + ], + resource: [ + 'checklist', + ], + }, + }, + description: 'The ID of the checklist to update.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'createCheckItem', + ], + resource: [ + 'checklist', + ], + }, + }, + description: 'The name of the new check item on the checklist.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'createCheckItem', + ], + resource: [ + 'checklist', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Checked', + name: 'checked', + type: 'boolean', + default: false, + description: 'Determines whether the check item is already checked when created.', + }, + { + displayName: 'Position', + name: 'pos', + type: 'string', + default: '', + description: 'The position of the checklist on the card. One of: top, bottom, or a positive number.', + }, + ], + }, + // ---------------------------------- // checklist:deleteCheckItem // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/Trello.node.ts b/packages/nodes-base/nodes/Trello/Trello.node.ts index 807c634d09..6781dbcca8 100644 --- a/packages/nodes-base/nodes/Trello/Trello.node.ts +++ b/packages/nodes-base/nodes/Trello/Trello.node.ts @@ -565,6 +565,21 @@ export class Trello implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; Object.assign(qs, additionalFields); + } else if (operation === 'createCheckItem') { + // ---------------------------------- + // createCheckItem + // ---------------------------------- + + requestMethod = 'POST'; + + const checklistId = this.getNodeParameter('checklistId', i) as string; + + endpoint = `checklists/${checklistId}/checkItems`; + + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, { name, ...additionalFields }); + } else if (operation === 'deleteCheckItem') { // ---------------------------------- // deleteCheckItem diff --git a/packages/nodes-base/nodes/Twitter/DirectMessageDescription.ts b/packages/nodes-base/nodes/Twitter/DirectMessageDescription.ts index 640051d0a8..ba652990f5 100644 --- a/packages/nodes-base/nodes/Twitter/DirectMessageDescription.ts +++ b/packages/nodes-base/nodes/Twitter/DirectMessageDescription.ts @@ -91,7 +91,7 @@ export const directMessageFields = [ name: 'attachment', type: 'string', default: 'data', - description: 'Name of the binary propertie which contain
data which should be added to directMessage as attachment.', + description: 'Name of the binary property which contain
data that should be added to the direct message as attachment.', }, ], }, diff --git a/packages/nodes-base/nodes/UProc/GenericFunctions.ts b/packages/nodes-base/nodes/UProc/GenericFunctions.ts index ca77d55f98..c322249ec2 100644 --- a/packages/nodes-base/nodes/UProc/GenericFunctions.ts +++ b/packages/nodes-base/nodes/UProc/GenericFunctions.ts @@ -20,7 +20,10 @@ export async function uprocApiRequest(this: IHookFunctions | IExecuteFunctions | } const token = Buffer.from(`${credentials.email}:${credentials.apiKey}`).toString('base64'); const options: OptionsWithUri = { - headers: { Authorization: `Basic ${token}` }, + headers: { + Authorization: `Basic ${token}`, + 'User-agent': 'n8n', + }, method, qs, body, diff --git a/packages/nodes-base/nodes/UProc/GroupDescription.ts b/packages/nodes-base/nodes/UProc/GroupDescription.ts index 07d046b703..f1483a0dac 100644 --- a/packages/nodes-base/nodes/UProc/GroupDescription.ts +++ b/packages/nodes-base/nodes/UProc/GroupDescription.ts @@ -1,6 +1,6 @@ import { IDataObject, - INodeProperties, + INodeProperties } from 'n8n-workflow'; import { diff --git a/packages/nodes-base/nodes/UProc/Json/Groups.ts b/packages/nodes-base/nodes/UProc/Json/Groups.ts index 8f18c4765f..693bc2d221 100644 --- a/packages/nodes-base/nodes/UProc/Json/Groups.ts +++ b/packages/nodes-base/nodes/UProc/Json/Groups.ts @@ -1,36 +1 @@ -export const groups = { - groups: [{ - 'translated': 'Audio', - 'name': 'audio', - }, { - 'translated': 'Communication', - 'name': 'communication', - }, { - 'translated': 'Company', - 'name': 'company', - }, { - 'translated': 'Finance', - 'name': 'finance', - }, { - 'translated': 'Geographical', - 'name': 'geographic', - }, { - 'translated': 'Image', - 'name': 'image', - }, { - 'translated': 'Internet', - 'name': 'internet', - }, { - 'translated': 'Personal', - 'name': 'personal', - }, { - 'translated': 'Product', - 'name': 'product', - }, { - 'translated': 'Security', - 'name': 'security', - }, { - 'translated': 'Text', - 'name': 'text', - }], -}; \ No newline at end of file +export const groups = {groups: [{'translated':'Audio','name':'audio'},{'translated':'Communication','name':'communication'},{'translated':'Company','name':'company'},{'translated':'Finance','name':'finance'},{'translated':'Geographical','name':'geographic'},{'translated':'Image','name':'image'},{'translated':'Internet','name':'internet'},{'translated':'Personal','name':'personal'},{'translated':'Product','name':'product'},{'translated':'Security','name':'security'},{'translated':'Text','name':'text'}]}; \ No newline at end of file diff --git a/packages/nodes-base/nodes/UProc/Json/Tools.ts b/packages/nodes-base/nodes/UProc/Json/Tools.ts index 738bfcedd3..36030e287c 100644 --- a/packages/nodes-base/nodes/UProc/Json/Tools.ts +++ b/packages/nodes-base/nodes/UProc/Json/Tools.ts @@ -1 +1 @@ -export const tools = {processors: [{'k':'checkCreditcardChecksum','d':'Check Card Number Has Valid Format','ed':'Check if credit card number checksum is valid (Visa, Mastercard, Diners Club, Carte Blanche, American Express, Discover, JCB, enRoute, Solo, Switch, Maestro, LaserCard, ChinaUnionPay, BankCard, Voyager)','g':'finance','p':[{'n':'credit_card','r':true,'t':'string','p':'4024007151839544'}]},{'k':'getCreditcardType','d':'Get Credit Card Type By Number','ed':'Get credit card type (Visa, Mastercard, Diners Club, Carte Blanche, American Express, Discover, JCB, enRoute, Solo, Switch, Maestro, LaserCard, ChinaUnionPay, BankCard, Voyager)','g':'finance','p':[{'n':'credit_card','r':true,'t':'string','p':'4024007151839544'}]},{'k':'getAddressBySearch','d':'Get Exact Address By Search','ed':'Get an exact address (street name, number, city, zipcode, province, region, country, latitude and longitude) by a partial address search','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120, Barcelona, España'}]},{'k':'getCoordinateBySearch','d':'Get Coordinates By Search','ed':'Discover latitude and longitude coordinates of a postal address','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Urgell, 120, Barcelona, España'}]},{'k':'checkAddressExist','d':'Check Exact Address Exists','ed':'Check if an exact address exists by a partial address search','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Urgell, 120, Barcelona'},{'n':'country','r':false,'t':'string','p':'ES'}]},{'k':'getAddressNormalized','d':'Get Normalized Address','ed':'Allow to normalize an address, removing non allowed characters','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120, Barcelona Spain'}]},{'k':'checkAddressNumberExist','d':'Check Street Number Exists','ed':'Check if a house number exists by a partial address search','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Urgell, 120, Barcelona'},{'n':'country','r':false,'t':'string','p':'ES'}]},{'k':'getAddressSplitted','d':'Get Parsed Address','ed':'Parse postal address into separated fields, getting a basic resolution','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120 08036 Barcelona Barcelona Spain'}]},{'k':'getAddressSplittedBest','d':'Get Improved Parsed Address','ed':'Parse postal address into separated fields, getting an improved resolution','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120 08036 Barcelona Barcelona Spain'}]},{'k':'checkCoordinateValid','d':'Check Valid Coordinates','ed':'Check if coordinates have a valid format','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.2522,-12.30'}]},{'k':'checkAgeBetw','d':'Check Age Between','ed':'Check if age is between two numbers','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years1','r':true,'t':'number','p':'35'},{'n':'years2','r':true,'t':'number','p':'50'}]},{'k':'getAgeByDate','d':'Get Age By Date','ed':'Discover the age of a birth date (multiple formats allowed)','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'checkAgeEq','d':'Check Ages Are Equal','ed':'Check if ages are equal','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'45'}]},{'k':'checkAgeGe','d':'Check Age Is Greater Or Equal','ed':'Check if age is greater or equal than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'43'}]},{'k':'checkAgeGt','d':'Check Age Is Greater','ed':'Check if age is greater than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'40'}]},{'k':'checkAgeIsAdult','d':'Check Age Is Greater Than Or Equal To 18 Years','ed':'Check if birth date belongs to an adult: 18 years old (Spain)','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1950-05-20'}]},{'k':'checkAgeIsForties','d':'Check Age Is Between 40 And 49 Years','ed':'Check if date returns an age between 40 and 49 years old','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'checkAgeIsRetired','d':'Check Age Is Greater Than 64 Years','ed':'Check if birth date returns an age greater than 64 years old','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1954-05-20'}]},{'k':'checkAgeIsTwenties','d':'Check Age Is Between 20 And 29 Years','ed':'Check if date returns an age between 20 and 29 years old','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'2000-05-20'}]},{'k':'checkAgeLe','d':'Check Age Is Lower Or Equal','ed':'Check if age is lower or equal than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'46'}]},{'k':'checkAgeLt','d':'Check Age Is Lower','ed':'Chekc if age is lower than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'46'}]},{'k':'getAgeRange','d':'Get Age Range By Date','ed':'Discover the age range of a person by birth date','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'checkAsinExist','d':'Check ASIN Exists','ed':'Check if a ASIN code exists on Amazon marketplace','g':'product','p':[{'n':'asin','r':true,'t':'string','p':'B00005N5PF'}]},{'k':'checkAsinValid','d':'Check ASIN Valid','ed':'Check if a ASIN code has a valid format','g':'product','p':[{'n':'asin','r':true,'t':'string','p':'B00005N5PF'}]},{'k':'getAudioAdvancedSpeechByText','d':'Get Advanced Speech By Text','ed':'Get advanced human audio file by provided text and language','g':'audio','p':[{'n':'text','r':true,'t':'string','p':'Hi! My name is Miquel. I will read any text you type here.'},{'n':'gender','r':true,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]},{'n':'language','r':true,'t':'options','p':'american','o':[{'name':'American','value':'american'},{'name':'Arabic','value':'arabic'},{'name':'Bengali','value':'bengali'},{'name':'British','value':'british'},{'name':'Czech','value':'czech'},{'name':'Danish','value':'danish'},{'name':'Dutch','value':'dutch'},{'name':'Filipino','value':'filipino'},{'name':'Finnish','value':'finnish'},{'name':'French','value':'french'},{'name':'German','value':'german'},{'name':'Greek','value':'greek'},{'name':'Gujurati','value':'gujurati'},{'name':'Hindi','value':'hindi'},{'name':'Hungarian','value':'hungarian'},{'name':'Indonesian','value':'indonesian'},{'name':'Italian','value':'italian'},{'name':'Japanese','value':'japanese'},{'name':'Kannada','value':'kannada'},{'name':'Korean','value':'korean'},{'name':'Malayalam','value':'malayalam'},{'name':'Mandarin','value':'mandarin'},{'name':'Norwegian','value':'norwegian'},{'name':'Polish','value':'polish'},{'name':'Portuguese','value':'portuguese'},{'name':'Russian','value':'russian'},{'name':'Slovak','value':'slovak'},{'name':'Spanish','value':'spanish'},{'name':'Tamil','value':'tamil'},{'name':'Telugu','value':'telugu'},{'name':'Thai','value':'thai'},{'name':'Turkish','value':'turkish'},{'name':'Ukranian','value':'ukranian'},{'name':'Vietnamese','value':'vietnamese'}]}]},{'k':'getAudioSpeechByText','d':'Get Speech By Text','ed':'Get audio file by provided text and language','g':'audio','p':[{'n':'text','r':true,'t':'string','p':'Hi! My name is Miquel. I will read any text you type here.'},{'n':'gender','r':true,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]},{'n':'language','r':true,'t':'options','p':'american','o':[{'name':'American','value':'american'},{'name':'French','value':'french'},{'name':'German','value':'german'},{'name':'Italian','value':'italian'},{'name':'Japanese','value':'japanese'},{'name':'Portuguese','value':'portuguese'},{'name':'Russian','value':'russian'},{'name':'Spanish','value':'spanish'}]}]},{'k':'checkBankAccountValidEs','d':'Check Bank Account Is Valid (ES)','ed':'Discover if account number has a valid format','g':'finance','p':[{'n':'account','r':true,'t':'string','p':'14650120311716144388'}]},{'k':'checkBankBicValid','d':'Check BIC Is Valid','ed':'Discover if BIC number has a valid format','g':'finance','p':[{'n':'bic','r':true,'t':'string','p':'DABAIE2D'}]},{'k':'getBankIbanByAccount','d':'Get IBAN By Account','ed':'Get IBAN number by account number of the country','g':'finance','p':[{'n':'account','r':true,'t':'string','p':'14650120311716144388'},{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getBankIbanLookup','d':'Get IBAN Lookup','ed':'Get to search data bank information by IBAN account number','g':'finance','p':[{'n':'iban','r':true,'t':'string','p':'NL91ABNA0417164300'}]},{'k':'checkBankIbanValid','d':'Check IBAN Is Valid','ed':'Discover if IBAN account number has a valid format','g':'finance','p':[{'n':'iban','r':true,'t':'string','p':'ES3314650120311716144388'}]},{'k':'getBarcodeEncoded','d':'Get Encoded Barcode','ed':'Get an encoded barcode by number and required standard','g':'image','p':[{'n':'text','r':true,'t':'string','p':'0635753490879'},{'n':'bcid','r':true,'t':'options','p':'ean13','o':[{'name':'Auspost','value':'auspost'},{'name':'Azteccode','value':'azteccode'},{'name':'Azteccodecompact','value':'azteccodecompact'},{'name':'Aztecrune','value':'aztecrune'},{'name':'Bc412','value':'bc412'},{'name':'Channelcode','value':'channelcode'},{'name':'Codablockf','value':'codablockf'},{'name':'Code11','value':'code11'},{'name':'Code128','value':'code128'},{'name':'Code16k','value':'code16k'},{'name':'Code2of5','value':'code2of5'},{'name':'Code32','value':'code32'},{'name':'Code39','value':'code39'},{'name':'Code39ext','value':'code39ext'},{'name':'Code49','value':'code49'},{'name':'Code93','value':'code93'},{'name':'Code93ext','value':'code93ext'},{'name':'Codeone','value':'codeone'},{'name':'Coop2of5','value':'coop2of5'},{'name':'Daft','value':'daft'},{'name':'Databarexpanded','value':'databarexpanded'},{'name':'Databarexpandedcomposite','value':'databarexpandedcomposite'},{'name':'Databarexpandedstacked','value':'databarexpandedstacked'},{'name':'Databarexpandedstackedcomposite','value':'databarexpandedstackedcomposite'},{'name':'Databarlimited','value':'databarlimited'},{'name':'Databarlimitedcomposite','value':'databarlimitedcomposite'},{'name':'Databaromni','value':'databaromni'},{'name':'Databaromnicomposite','value':'databaromnicomposite'},{'name':'Databarstacked','value':'databarstacked'},{'name':'Databarstackedcomposite','value':'databarstackedcomposite'},{'name':'Databarstackedomni','value':'databarstackedomni'},{'name':'Databarstackedomnicomposite','value':'databarstackedomnicomposite'},{'name':'Databartruncated','value':'databartruncated'},{'name':'Databartruncatedcomposite','value':'databartruncatedcomposite'},{'name':'Datalogic2of5','value':'datalogic2of5'},{'name':'Datamatrix','value':'datamatrix'},{'name':'Datamatrixrectangular','value':'datamatrixrectangular'},{'name':'Dotcode','value':'dotcode'},{'name':'Ean13','value':'ean13'},{'name':'Ean13composite','value':'ean13composite'},{'name':'Ean14','value':'ean14'},{'name':'Ean2','value':'ean2'},{'name':'Ean5','value':'ean5'},{'name':'Ean8','value':'ean8'},{'name':'Ean8composite','value':'ean8composite'},{'name':'Flattermarken','value':'flattermarken'},{'name':'Gs1-128','value':'gs1-128'},{'name':'Gs1-128composite','value':'gs1-128composite'},{'name':'Gs1-cc','value':'gs1-cc'},{'name':'Gs1datamatrix','value':'gs1datamatrix'},{'name':'Gs1datamatrixrectangular','value':'gs1datamatrixrectangular'},{'name':'Gs1northamericancoupon','value':'gs1northamericancoupon'},{'name':'Hanxin','value':'hanxin'},{'name':'Hibcazteccode','value':'hibcazteccode'},{'name':'Hibccodablockf','value':'hibccodablockf'},{'name':'Hibccode128','value':'hibccode128'},{'name':'Hibccode39','value':'hibccode39'},{'name':'Hibcdatamatrix','value':'hibcdatamatrix'},{'name':'Hibcdatamatrixrectangular','value':'hibcdatamatrixrectangular'},{'name':'Hibcmicropdf417','value':'hibcmicropdf417'},{'name':'Hibcpdf417','value':'hibcpdf417'},{'name':'Iata2of5','value':'iata2of5'},{'name':'Identcode','value':'identcode'},{'name':'Industrial2of5','value':'industrial2of5'},{'name':'Interleaved2of5','value':'interleaved2of5'},{'name':'Isbn','value':'isbn'},{'name':'Ismn','value':'ismn'},{'name':'Issn','value':'issn'},{'name':'Itf14','value':'itf14'},{'name':'Japanpost','value':'japanpost'},{'name':'Kix','value':'kix'},{'name':'Leitcode','value':'leitcode'},{'name':'Matrix2of5','value':'matrix2of5'},{'name':'Maxicode','value':'maxicode'},{'name':'Micropdf417','value':'micropdf417'},{'name':'Msi','value':'msi'},{'name':'Onecode','value':'onecode'},{'name':'Pdf417','value':'pdf417'},{'name':'Pdf417compact','value':'pdf417compact'},{'name':'Pharmacode','value':'pharmacode'},{'name':'Pharmacode2','value':'pharmacode2'},{'name':'Planet','value':'planet'},{'name':'Plessey','value':'plessey'},{'name':'Posicode','value':'posicode'},{'name':'Postnet','value':'postnet'},{'name':'Pzn','value':'pzn'},{'name':'RationalizedCodabar','value':'rationalizedCodabar'},{'name':'Raw','value':'raw'},{'name':'Royalmail','value':'royalmail'},{'name':'Sscc18','value':'sscc18'},{'name':'Symbol','value':'symbol'},{'name':'Telepen','value':'telepen'},{'name':'Telepennumeric','value':'telepennumeric'},{'name':'Ultracode','value':'ultracode'},{'name':'Upca','value':'upca'},{'name':'Upcacomposite','value':'upcacomposite'},{'name':'Upce','value':'upce'},{'name':'Upcecomposite','value':'upcecomposite'}]}]},{'k':'getBookAuthorLookup','d':'Get Book By Author','ed':'Get book by author\'s surname','g':'product','p':[{'n':'author','r':true,'t':'string','p':'Albert Einstein'}]},{'k':'getBookCategoryLookup','d':'Get Book By Category','ed':'Get all publications by category','g':'product','p':[{'n':'category','r':true,'t':'string','p':'science'}]},{'k':'checkBookIsbn','d':'Check ISBN Code Is Valid','ed':'Allow to check if an ISBN10/13 code has a valid format','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'0306406152'}]},{'k':'checkBookIsbnExist','d':'Check ISBN Code Exists','ed':'Allow to check if an ISBN book exist','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'9780306406157'}]},{'k':'getBookIsbnLookup','d':'Get Book By ISBN','ed':'Get book or publication data by 10 or 13 digits ISBN code','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'9780306406157'}]},{'k':'checkBookIsbn10','d':'Check ISBN10 Code Is Valid','ed':'Allow to check if an ISBN10 code has a valid format','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'0306406152'}]},{'k':'checkBookIsbn13','d':'Check ISBN13 Code Is Valid','ed':'Allow to check if an ISBN13 code has a valid format','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'9780306406157'}]},{'k':'getBookListAuthorLookup','d':'Get Books By Author','ed':'Get books by author\'s surname','g':'product','p':[{'n':'author','r':true,'t':'string','p':'Albert Einstein'}]},{'k':'getBookListCategoryLookup','d':'Get Books By Category','ed':'Get all books by category','g':'product','p':[{'n':'category','r':true,'t':'string','p':'science'}]},{'k':'getBookListPublisherLookup','d':'Get Books By Editor','ed':'Get all books by editor','g':'product','p':[{'n':'publisher','r':true,'t':'string','p':'Grupo RBA'}]},{'k':'getBookListTitleLookup','d':'Get Books By Title','ed':'Get all books by title','g':'product','p':[{'n':'title','r':true,'t':'string','p':'Science'}]},{'k':'getBookPublisherLookup','d':'Get Book By Editor','ed':'Get book data by editor\'s name','g':'product','p':[{'n':'publisher','r':true,'t':'string','p':'Grupo RBA'}]},{'k':'getBookTitleLookup','d':'Get Book By Title','ed':'Get book data by title','g':'product','p':[{'n':'title','r':true,'t':'string','p':'La empresa más feliz del mundo'}]},{'k':'getNifByDni','d':'Get NIF By DNI (ES)','ed':'Discover the letter of a dni card number','g':'personal','p':[{'n':'dni','r':true,'t':'string','p':'44016116'}]},{'k':'getCifNormalized','d':'Get Normalized CIF','ed':'Allow to normalize a CIF number, removing non allowed characters','g':'company','p':[{'n':'cif','r':true,'t':'string','p':'B 62084 959'}]},{'k':'getDniNormalized','d':'Get Normalized DNI (ES)','ed':'Allow to normalize a DNI number, removing non allowed characters','g':'personal','p':[{'n':'dni','r':true,'t':'string','p':'44016a116'}]},{'k':'getNieNormalized','d':'Get Normalized NIE (ES)','ed':'Allow to normalize a NIE number, removing non allowed characters','g':'personal','p':[{'n':'nie','r':true,'t':'string','p':'X402001 122g'}]},{'k':'getNifNormalized','d':'Get Normalized NIF (ES)','ed':'Allow to normalize a NIF number, removing non allowed characters','g':'personal','p':[{'n':'nif','r':true,'t':'string','p':'402001 122g'}]},{'k':'checkCifValid','d':'Check CIF Is Valid (ES)','ed':'Discover if a cif card number is valid','g':'company','p':[{'n':'cif','r':true,'t':'string','p':'A58818501'}]},{'k':'checkDniValid','d':'Check Valid Dni (ES)','ed':'Discover if a dni card number is valid','g':'personal','p':[{'n':'dni','r':true,'t':'string','p':'44016116'}]},{'k':'checkNieValid','d':'Check NIE Is Valid (ES)','ed':'Discover if a NIE card number is valid','g':'personal','p':[{'n':'nie','r':true,'t':'string','p':'Y2918527W'}]},{'k':'checkNifValid','d':'Check NIF Is Valid (ES)','ed':'Discover if a nif card number is valid','g':'personal','p':[{'n':'nif','r':true,'t':'string','p':'44016116G'}]},{'k':'getCityByIp','d':'Get City By IP','ed':'Get city from ip','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCityByName','d':'Get City By Name (ES)','ed':'City search by partial name (only Spain)','g':'geographic','p':[{'n':'city','r':true,'t':'string','p':'Bar'}]},{'k':'getCityByPhone','d':'Get City By Phone (ES)','ed':'Discover the city name by the local phone number (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'932187670'}]},{'k':'getCityByZipcode','d':'Get City By Zipcode (ES)','ed':'Discover the city name by the zipcode (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getCityListByName','d':'Get Cities By Prefix (ES)','ed':'Get multiple cities by partial initial text (only Spain)','g':'geographic','p':[{'n':'city','r':true,'t':'string','p':'Barce'}]},{'k':'getCityListByPhone','d':'Get Cities By Phone (ES)','ed':'Get multiple cities by phone prefix (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'938499145'}]},{'k':'getCityListByZipcode','d':'Get Cities By Zipcode (ES)','ed':'Get multiple cities by zipcode prefix (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'234'}]},{'k':'getCityNormalized','d':'Get Normalized City','ed':'Allow to normalize a city, removing non allowed characters','g':'geographic','p':[{'n':'city','r':true,'t':'string','p':'Barc3l0na'}]},{'k':'checkEan13Valid','d':'Check EAN13 Is Valid','ed':'Check if a EAN barcode of 13 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'4006381333931'}]},{'k':'checkGtin13Valid','d':'Check GTIN13 Is Valid','ed':'Check if a GTIN barcode of 13 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'4006381333931'}]},{'k':'checkEan14Valid','d':'Check EAN14 Is Valid','ed':'Check if a EAN barcode of 14 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'04006381333931'}]},{'k':'checkGtin14Valid','d':'Check GTIN14 Is Valid','ed':'Check if a GTIN barcode of 14 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'04006381333931'}]},{'k':'checkEan18Valid','d':'Check EAN18 Is Valid','ed':'Check if a EAN barcode of 18 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'000004006381333931'}]},{'k':'checkEan8Valid','d':'Check EAN8 Is Valid','ed':'Check if a EAN barcode of 8 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'checkGtin8Valid','d':'Check GTIN8 Is Valid','ed':'Check if a GTIN barcode of 8 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'checkEanExist','d':'Check EAN Exists','ed':'Check if a EAN code exists on Amazon Marketplace (.com supported)','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'0635753490879'}]},{'k':'checkUpcExist','d':'Check UPC Exists','ed':'Check if a UPC code exists','g':'product','p':[{'n':'upc','r':true,'t':'string','p':'635753490879'}]},{'k':'checkUpcFormat','d':'Check UPC Has Valid Format','ed':'Check if a UPC code has a valid format','g':'product','p':[{'n':'upc','r':true,'t':'string','p':'635753490879'}]},{'k':'checkNumberIsin','d':'Check ISIN Code Is Valid','ed':'Check if ISIN number is valid','g':'company','p':[{'n':'isin','r':true,'t':'string','p':'US0378331005'}]},{'k':'getUpcLookup','d':'Get Product By UPC','ed':'Get product data of an UPC code on Amazon Marketplace (.com supported)','g':'product','p':[{'n':'upc','r':true,'t':'string','p':'635753490879'}]},{'k':'checkNumberSsEs','d':'Check Social Security Number Is Valid (ES)','ed':'Check if SS number is valid, only for Spain','g':'company','p':[{'n':'number','r':true,'t':'string','p':'998239812282'}]},{'k':'checkNumberUuid','d':'Check UUID Number Is Valid','ed':'Check if it a valid UUID number','g':'security','p':[{'n':'uuid','r':true,'t':'string','p':'550e8400-e29b-41d4-a716-446655440000'}]},{'k':'checkEanValid','d':'Check EAN Is Valid','ed':'Check if a EAN barcode (8 or 13 digits) has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'checkGtinValid','d':'Check GTIN Is Valid','ed':'Check if a GTIN barcode (8 or 13 digits) has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'getCommunityByZipcode','d':'Get Community By Zipcode (ES)','ed':'Discover the community name from a zipcode number (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getCompanyByCif','d':'Get Company By CIF (ES)','ed':'Get company data by CIF','g':'company','p':[{'n':'cif','r':true,'t':'string','p':'B66998592'}]},{'k':'getCompanyByDomain','d':'Get Company By Domain','ed':'Get company data by domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'}]},{'k':'getCompanyByDuns','d':'Get Company By DUNS (ES)','ed':'Get company data by DUNS','g':'company','p':[{'n':'duns','r':true,'t':'string','p':'464016690'}]},{'k':'getCompanyByEmail','d':'Get Company By Email','ed':'Get company data by email','g':'company','p':[{'n':'email','r':true,'t':'string','p':'hello@killia.com'}]},{'k':'getCompanyByIp','d':'Get Company By IP','ed':'Get company data by IP address','g':'company','p':[{'n':'ip','r':true,'t':'string','p':'74.125.228.72'}]},{'k':'getCompanyByName','d':'Get Company By Name','ed':'Get company data by name','g':'company','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick S.L.'},{'n':'country','r':false,'t':'options','p':'Spain','o':[{'name':'Afghanistan','value':'Afghanistan'},{'name':'Albania','value':'Albania'},{'name':'Algeria','value':'Algeria'},{'name':'American Samoa','value':'American Samoa'},{'name':'Andorra','value':'Andorra'},{'name':'Angola','value':'Angola'},{'name':'Anguilla','value':'Anguilla'},{'name':'Antarctica','value':'Antarctica'},{'name':'Antigua And Barbuda','value':'Antigua And Barbuda'},{'name':'Argentina','value':'Argentina'},{'name':'Armenia','value':'Armenia'},{'name':'Aruba','value':'Aruba'},{'name':'Australia','value':'Australia'},{'name':'Austria','value':'Austria'},{'name':'Azerbaijan','value':'Azerbaijan'},{'name':'Bahamas','value':'Bahamas'},{'name':'Bahrain','value':'Bahrain'},{'name':'Bangladesh','value':'Bangladesh'},{'name':'Barbados','value':'Barbados'},{'name':'Belarus','value':'Belarus'},{'name':'Belgium','value':'Belgium'},{'name':'Belize','value':'Belize'},{'name':'Benin','value':'Benin'},{'name':'Bermuda','value':'Bermuda'},{'name':'Bhutan','value':'Bhutan'},{'name':'Bolivia','value':'Bolivia'},{'name':'Bosnia And Herzegovina','value':'Bosnia And Herzegovina'},{'name':'Botswana','value':'Botswana'},{'name':'Bouvet Island','value':'Bouvet Island'},{'name':'Brazil','value':'Brazil'},{'name':'British Indian Ocean Territory','value':'British Indian Ocean Territory'},{'name':'Brunei Darussalam','value':'Brunei Darussalam'},{'name':'Bulgaria','value':'Bulgaria'},{'name':'Burkina Faso','value':'Burkina Faso'},{'name':'Burundi','value':'Burundi'},{'name':'Cambodia','value':'Cambodia'},{'name':'Cameroon','value':'Cameroon'},{'name':'Canada','value':'Canada'},{'name':'Cape Verde','value':'Cape Verde'},{'name':'Cayman Islands','value':'Cayman Islands'},{'name':'Central African Republic','value':'Central African Republic'},{'name':'Chad','value':'Chad'},{'name':'Chile','value':'Chile'},{'name':'China','value':'China'},{'name':'Christmas Island','value':'Christmas Island'},{'name':'Cocos (keeling) Islands','value':'Cocos (keeling) Islands'},{'name':'Colombia','value':'Colombia'},{'name':'Comoros','value':'Comoros'},{'name':'Congo','value':'Congo'},{'name':'Congo, The Democratic Republic Of The','value':'Congo, The Democratic Republic Of The'},{'name':'Cook Islands','value':'Cook Islands'},{'name':'Costa Rica','value':'Costa Rica'},{'name':'Cote D\'ivoire','value':'Cote D\'ivoire'},{'name':'Croatia','value':'Croatia'},{'name':'Cuba','value':'Cuba'},{'name':'Cyprus','value':'Cyprus'},{'name':'Czech Republic','value':'Czech Republic'},{'name':'Denmark','value':'Denmark'},{'name':'Djibouti','value':'Djibouti'},{'name':'Dominica','value':'Dominica'},{'name':'Dominican Republic','value':'Dominican Republic'},{'name':'East Timor','value':'East Timor'},{'name':'Ecuador','value':'Ecuador'},{'name':'Egypt','value':'Egypt'},{'name':'El Salvador','value':'El Salvador'},{'name':'Equatorial Guinea','value':'Equatorial Guinea'},{'name':'Eritrea','value':'Eritrea'},{'name':'Estonia','value':'Estonia'},{'name':'Ethiopia','value':'Ethiopia'},{'name':'Falkland Islands (malvinas)','value':'Falkland Islands (malvinas)'},{'name':'Faroe Islands','value':'Faroe Islands'},{'name':'Fiji','value':'Fiji'},{'name':'Finland','value':'Finland'},{'name':'France','value':'France'},{'name':'French Guiana','value':'French Guiana'},{'name':'French Polynesia','value':'French Polynesia'},{'name':'French Southern Territories','value':'French Southern Territories'},{'name':'Gabon','value':'Gabon'},{'name':'Gambia','value':'Gambia'},{'name':'Georgia','value':'Georgia'},{'name':'Germany','value':'Germany'},{'name':'Ghana','value':'Ghana'},{'name':'Gibraltar','value':'Gibraltar'},{'name':'Greece','value':'Greece'},{'name':'Greenland','value':'Greenland'},{'name':'Grenada','value':'Grenada'},{'name':'Guadeloupe','value':'Guadeloupe'},{'name':'Guam','value':'Guam'},{'name':'Guatemala','value':'Guatemala'},{'name':'Guinea','value':'Guinea'},{'name':'Guinea-bissau','value':'Guinea-bissau'},{'name':'Guyana','value':'Guyana'},{'name':'Haiti','value':'Haiti'},{'name':'Heard Island And Mcdonald Islands','value':'Heard Island And Mcdonald Islands'},{'name':'Holy See (vatican City State)','value':'Holy See (vatican City State)'},{'name':'Honduras','value':'Honduras'},{'name':'Hong Kong','value':'Hong Kong'},{'name':'Hungary','value':'Hungary'},{'name':'Iceland','value':'Iceland'},{'name':'India','value':'India'},{'name':'Indonesia','value':'Indonesia'},{'name':'Iran, Islamic Republic Of','value':'Iran, Islamic Republic Of'},{'name':'Iraq','value':'Iraq'},{'name':'Ireland','value':'Ireland'},{'name':'Israel','value':'Israel'},{'name':'Italy','value':'Italy'},{'name':'Jamaica','value':'Jamaica'},{'name':'Japan','value':'Japan'},{'name':'Jordan','value':'Jordan'},{'name':'Kazakstan','value':'Kazakstan'},{'name':'Kenya','value':'Kenya'},{'name':'Kiribati','value':'Kiribati'},{'name':'Korea, Democratic People\'s Republic Of','value':'Korea, Democratic People\'s Republic Of'},{'name':'Korea, Republic Of','value':'Korea, Republic Of'},{'name':'Kosovo','value':'Kosovo'},{'name':'Kuwait','value':'Kuwait'},{'name':'Kyrgyzstan','value':'Kyrgyzstan'},{'name':'Lao People\'s Democratic Republic','value':'Lao People\'s Democratic Republic'},{'name':'Latvia','value':'Latvia'},{'name':'Lebanon','value':'Lebanon'},{'name':'Lesotho','value':'Lesotho'},{'name':'Liberia','value':'Liberia'},{'name':'Libyan Arab Jamahiriya','value':'Libyan Arab Jamahiriya'},{'name':'Liechtenstein','value':'Liechtenstein'},{'name':'Lithuania','value':'Lithuania'},{'name':'Luxembourg','value':'Luxembourg'},{'name':'Macau','value':'Macau'},{'name':'Macedonia, The Former Yugoslav Republic Of','value':'Macedonia, The Former Yugoslav Republic Of'},{'name':'Madagascar','value':'Madagascar'},{'name':'Malawi','value':'Malawi'},{'name':'Malaysia','value':'Malaysia'},{'name':'Maldives','value':'Maldives'},{'name':'Mali','value':'Mali'},{'name':'Malta','value':'Malta'},{'name':'Marshall Islands','value':'Marshall Islands'},{'name':'Martinique','value':'Martinique'},{'name':'Mauritania','value':'Mauritania'},{'name':'Mauritius','value':'Mauritius'},{'name':'Mayotte','value':'Mayotte'},{'name':'Mexico','value':'Mexico'},{'name':'Micronesia, Federated States Of','value':'Micronesia, Federated States Of'},{'name':'Moldova, Republic Of','value':'Moldova, Republic Of'},{'name':'Monaco','value':'Monaco'},{'name':'Mongolia','value':'Mongolia'},{'name':'Montenegro','value':'Montenegro'},{'name':'Montserrat','value':'Montserrat'},{'name':'Morocco','value':'Morocco'},{'name':'Mozambique','value':'Mozambique'},{'name':'Myanmar','value':'Myanmar'},{'name':'Namibia','value':'Namibia'},{'name':'Nauru','value':'Nauru'},{'name':'Nepal','value':'Nepal'},{'name':'Netherlands','value':'Netherlands'},{'name':'Netherlands Antilles','value':'Netherlands Antilles'},{'name':'New Caledonia','value':'New Caledonia'},{'name':'New Zealand','value':'New Zealand'},{'name':'Nicaragua','value':'Nicaragua'},{'name':'Niger','value':'Niger'},{'name':'Nigeria','value':'Nigeria'},{'name':'Niue','value':'Niue'},{'name':'Norfolk Island','value':'Norfolk Island'},{'name':'Northern Mariana Islands','value':'Northern Mariana Islands'},{'name':'Norway','value':'Norway'},{'name':'Oman','value':'Oman'},{'name':'Pakistan','value':'Pakistan'},{'name':'Palau','value':'Palau'},{'name':'Palestinian Territory, Occupied','value':'Palestinian Territory, Occupied'},{'name':'Panama','value':'Panama'},{'name':'Papua New Guinea','value':'Papua New Guinea'},{'name':'Paraguay','value':'Paraguay'},{'name':'Peru','value':'Peru'},{'name':'Philippines','value':'Philippines'},{'name':'Pitcairn','value':'Pitcairn'},{'name':'Poland','value':'Poland'},{'name':'Portugal','value':'Portugal'},{'name':'Puerto Rico','value':'Puerto Rico'},{'name':'Qatar','value':'Qatar'},{'name':'Reunion','value':'Reunion'},{'name':'Romania','value':'Romania'},{'name':'Russian Federation','value':'Russian Federation'},{'name':'Rwanda','value':'Rwanda'},{'name':'Saint Helena','value':'Saint Helena'},{'name':'Saint Kitts And Nevis','value':'Saint Kitts And Nevis'},{'name':'Saint Lucia','value':'Saint Lucia'},{'name':'Saint Pierre And Miquelon','value':'Saint Pierre And Miquelon'},{'name':'Saint Vincent And The Grenadines','value':'Saint Vincent And The Grenadines'},{'name':'Samoa','value':'Samoa'},{'name':'San Marino','value':'San Marino'},{'name':'Sao Tome And Principe','value':'Sao Tome And Principe'},{'name':'Saudi Arabia','value':'Saudi Arabia'},{'name':'Senegal','value':'Senegal'},{'name':'Serbia','value':'Serbia'},{'name':'Seychelles','value':'Seychelles'},{'name':'Sierra Leone','value':'Sierra Leone'},{'name':'Singapore','value':'Singapore'},{'name':'Slovakia','value':'Slovakia'},{'name':'Slovenia','value':'Slovenia'},{'name':'Solomon Islands','value':'Solomon Islands'},{'name':'Somalia','value':'Somalia'},{'name':'South Africa','value':'South Africa'},{'name':'South Georgia And The South Sandwich Islands','value':'South Georgia And The South Sandwich Islands'},{'name':'Spain','value':'Spain'},{'name':'Sri Lanka','value':'Sri Lanka'},{'name':'Sudan','value':'Sudan'},{'name':'Suriname','value':'Suriname'},{'name':'Svalbard And Jan Mayen','value':'Svalbard And Jan Mayen'},{'name':'Swaziland','value':'Swaziland'},{'name':'Sweden','value':'Sweden'},{'name':'Switzerland','value':'Switzerland'},{'name':'Syrian Arab Republic','value':'Syrian Arab Republic'},{'name':'Taiwan, Province Of China','value':'Taiwan, Province Of China'},{'name':'Tajikistan','value':'Tajikistan'},{'name':'Tanzania, United Republic Of','value':'Tanzania, United Republic Of'},{'name':'Thailand','value':'Thailand'},{'name':'Togo','value':'Togo'},{'name':'Tokelau','value':'Tokelau'},{'name':'Tonga','value':'Tonga'},{'name':'Trinidad And Tobago','value':'Trinidad And Tobago'},{'name':'Tunisia','value':'Tunisia'},{'name':'Turkey','value':'Turkey'},{'name':'Turkmenistan','value':'Turkmenistan'},{'name':'Turks And Caicos Islands','value':'Turks And Caicos Islands'},{'name':'Tuvalu','value':'Tuvalu'},{'name':'Uganda','value':'Uganda'},{'name':'Ukraine','value':'Ukraine'},{'name':'United Arab Emirates','value':'United Arab Emirates'},{'name':'United Kingdom','value':'United Kingdom'},{'name':'United States','value':'United States'},{'name':'United States Minor Outlying Islands','value':'United States Minor Outlying Islands'},{'name':'Uruguay','value':'Uruguay'},{'name':'Uzbekistan','value':'Uzbekistan'},{'name':'Vanuatu','value':'Vanuatu'},{'name':'Venezuela','value':'Venezuela'},{'name':'Viet Nam','value':'Viet Nam'},{'name':'Virgin Islands, British','value':'Virgin Islands, British'},{'name':'Virgin Islands, U.s.','value':'Virgin Islands, U.s.'},{'name':'Wallis And Futuna','value':'Wallis And Futuna'},{'name':'Western Sahara','value':'Western Sahara'},{'name':'Yemen','value':'Yemen'},{'name':'Zambia','value':'Zambia'},{'name':'Zimbabwe','value':'Zimbabwe'}]}]},{'k':'getCompanyByPhone','d':'Get Company By Phone','ed':'Get company data by phone number','g':'company','p':[{'n':'phone','r':true,'t':'string','p':'34933197570'}]},{'k':'getCompanyByProfile','d':'Get Company By Social Profile','ed':'Get company data by social network uri (LinkedIn, Twitter, ...)','g':'company','p':[{'n':'url','r':true,'t':'string','p':'https://twitter.com/Cloudflare'}]},{'k':'getPersonByProfile','d':'Get Person By Social Profile','ed':'Get personal data by social network profile','g':'personal','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getRoleClassified','d':'Get Classified Role','ed':'Identify and classify a prospect role detecting the right area and seniority to filter later','g':'company','p':[{'n':'role','r':true,'t':'string','p':'Project Manager'}]},{'k':'checkCompanyDebtorByTaxid','d':'Check Company Is Debtor By TaxId (ES)','ed':'Check if company is debtor by TaxId','g':'company','p':[{'n':'taxid','r':true,'t':'string','p':'B04363115'}]},{'k':'getPersonDecisionMaker','d':'Get Decision Maker','ed':'Get professional data of a decision maker by company name/domain and area','g':'company','p':[{'n':'company','r':true,'t':'string','p':'uproc.io'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]}]},{'k':'getPersonDecisionMakerBySearch','d':'Get Decision Maker By Search Engine','ed':'Discover the more suitable decision maker using search engines (Bing) by company name and area (optional)','g':'company','p':[{'n':'company','r':true,'t':'string','p':'oracle'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Ecommerce','value':'Ecommerce'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]},{'n':'clevel','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]},{'n':'location','r':false,'t':'string','p':''},{'n':'keyword','r':false,'t':'string','p':''}]},{'k':'getCompanyDomainByName','d':'Get Domain By Company Name','ed':'Get company domain by company name','g':'company','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getPersonEmailsByDomainAndArea','d':'Get Decision Maker\'s Emails By Domain And Area','ed':'Get professional emails of decision makers (by priority: executive, manager and directors) by company domain and area','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'oracle.com'},{'n':'area','r':true,'t':'options','p':'Marketing','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]}]},{'k':'getCompanyExtendedByDomain','d':'Get Company (Extended) By Domain','ed':'Get company contact, social and technology data by domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'}]},{'k':'getCompanyExtendedByEmail','d':'Get Company (Extended) By Email','ed':'Get company contact, social and technology data by email','g':'company','p':[{'n':'email','r':true,'t':'string','p':'hello@killia.com'}]},{'k':'getPersonExtendedByProfile','d':'Get Person (Extended) By Profile','ed':'Get personal and social data by social profile','g':'personal','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getProfileFacebookByCompany','d':'Get Facebook URI By Company','ed':'Get Facebook company profile by name without manual search on Google or Facebook.

This tool uses search engines (Bing and Google) through proxies','g':'company','p':[{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getCompanyFinancialByDomain','d':'Get Sales Data By Company\'s Domain','ed':'Get company sales data by company\'s domain name.

Next countries are supported: Spain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getCompanyFinancialByDuns','d':'Get Sales Data By Company\'s DUNS','ed':'Get company sales data by company\'s DUNS number.

Next countries are supported: Spain','g':'company','p':[{'n':'duns','r':true,'t':'string','p':'461809423'}]},{'k':'getCompanyFinancialByName','d':'Get Sales Data By Company\'s Name','ed':'Get company sales data by company\'s name.

Next countries are supported: Spain','g':'company','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick S.L.'}]},{'k':'getCompanyFinancialByTaxid','d':'Get Sales Data By Company\'s Taxid','ed':'Get company sales data by company\'s taxid (CIF).

Next countries are supported: Spain','g':'company','p':[{'n':'taxid','r':true,'t':'string','p':'B62084959'}]},{'k':'getCompanyGeocodedByIp','d':'Get Geocoded Company By IP','ed':'Get geocoded company data by IP address','g':'company','p':[{'n':'ip','r':true,'t':'string','p':'74.125.228.72'}]},{'k':'sendLinkedinInvitation','d':'Send Connection Request To Linkedin Profile','ed':'Send a custom message invitation to a non connected Linkedin profile (supported uris: https://www.linkedin.com/in/USERID)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'message','r':true,'t':'string','p':'Hi {{first}}, would you like to be part of my network! Thanks'}]},{'k':'sendLinkedinInvitationOrMessage','d':'Send Invitation Or Message To Linkedin Profile','ed':'Send a custom invitation message (parameter message1) if profile is connected or a custom message (parameter message2) otherwise (supported uris: https://www.linkedin.com/in/USERID)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'message1','r':true,'t':'string','p':'Hi {{first}}, would you like to be part of my network? Thanks'},{'n':'message2','r':true,'t':'string','p':'Hi {{first}}, thank you for accepting my invitation!'}]},{'k':'getProfileLinkedinByCompany','d':'Get LinkedIn URI By Company','ed':'Get LinkedIn company profile by name without manual search on Google or LinkedIn.

This tool uses search engines (Bing and Google) through proxies','g':'company','p':[{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getPersonListByParams','d':'Get Employees By Parameters','ed':'Get employees by company name or domain, area, seniority and country','g':'company','p':[{'n':'country','r':true,'t':'options','p':'Spain','o':[{'name':'Afghanistan','value':'Afghanistan'},{'name':'Albania','value':'Albania'},{'name':'Algeria','value':'Algeria'},{'name':'American Samoa','value':'American Samoa'},{'name':'Andorra','value':'Andorra'},{'name':'Angola','value':'Angola'},{'name':'Anguilla','value':'Anguilla'},{'name':'Antarctica','value':'Antarctica'},{'name':'Antigua And Barbuda','value':'Antigua And Barbuda'},{'name':'Argentina','value':'Argentina'},{'name':'Armenia','value':'Armenia'},{'name':'Aruba','value':'Aruba'},{'name':'Australia','value':'Australia'},{'name':'Austria','value':'Austria'},{'name':'Azerbaijan','value':'Azerbaijan'},{'name':'Bahamas','value':'Bahamas'},{'name':'Bahrain','value':'Bahrain'},{'name':'Bangladesh','value':'Bangladesh'},{'name':'Barbados','value':'Barbados'},{'name':'Belarus','value':'Belarus'},{'name':'Belgium','value':'Belgium'},{'name':'Belize','value':'Belize'},{'name':'Benin','value':'Benin'},{'name':'Bermuda','value':'Bermuda'},{'name':'Bhutan','value':'Bhutan'},{'name':'Bolivia','value':'Bolivia'},{'name':'Bosnia And Herzegovina','value':'Bosnia And Herzegovina'},{'name':'Botswana','value':'Botswana'},{'name':'Bouvet Island','value':'Bouvet Island'},{'name':'Brazil','value':'Brazil'},{'name':'British Indian Ocean Territory','value':'British Indian Ocean Territory'},{'name':'Brunei Darussalam','value':'Brunei Darussalam'},{'name':'Bulgaria','value':'Bulgaria'},{'name':'Burkina Faso','value':'Burkina Faso'},{'name':'Burundi','value':'Burundi'},{'name':'Cambodia','value':'Cambodia'},{'name':'Cameroon','value':'Cameroon'},{'name':'Canada','value':'Canada'},{'name':'Cape Verde','value':'Cape Verde'},{'name':'Cayman Islands','value':'Cayman Islands'},{'name':'Central African Republic','value':'Central African Republic'},{'name':'Chad','value':'Chad'},{'name':'Chile','value':'Chile'},{'name':'China','value':'China'},{'name':'Christmas Island','value':'Christmas Island'},{'name':'Cocos (keeling) Islands','value':'Cocos (keeling) Islands'},{'name':'Colombia','value':'Colombia'},{'name':'Comoros','value':'Comoros'},{'name':'Congo','value':'Congo'},{'name':'Congo, The Democratic Republic Of The','value':'Congo, The Democratic Republic Of The'},{'name':'Cook Islands','value':'Cook Islands'},{'name':'Costa Rica','value':'Costa Rica'},{'name':'Cote D\'ivoire','value':'Cote D\'ivoire'},{'name':'Croatia','value':'Croatia'},{'name':'Cuba','value':'Cuba'},{'name':'Cyprus','value':'Cyprus'},{'name':'Czech Republic','value':'Czech Republic'},{'name':'Denmark','value':'Denmark'},{'name':'Djibouti','value':'Djibouti'},{'name':'Dominica','value':'Dominica'},{'name':'Dominican Republic','value':'Dominican Republic'},{'name':'East Timor','value':'East Timor'},{'name':'Ecuador','value':'Ecuador'},{'name':'Egypt','value':'Egypt'},{'name':'El Salvador','value':'El Salvador'},{'name':'Equatorial Guinea','value':'Equatorial Guinea'},{'name':'Eritrea','value':'Eritrea'},{'name':'Estonia','value':'Estonia'},{'name':'Ethiopia','value':'Ethiopia'},{'name':'Falkland Islands (malvinas)','value':'Falkland Islands (malvinas)'},{'name':'Faroe Islands','value':'Faroe Islands'},{'name':'Fiji','value':'Fiji'},{'name':'Finland','value':'Finland'},{'name':'France','value':'France'},{'name':'French Guiana','value':'French Guiana'},{'name':'French Polynesia','value':'French Polynesia'},{'name':'French Southern Territories','value':'French Southern Territories'},{'name':'Gabon','value':'Gabon'},{'name':'Gambia','value':'Gambia'},{'name':'Georgia','value':'Georgia'},{'name':'Germany','value':'Germany'},{'name':'Ghana','value':'Ghana'},{'name':'Gibraltar','value':'Gibraltar'},{'name':'Greece','value':'Greece'},{'name':'Greenland','value':'Greenland'},{'name':'Grenada','value':'Grenada'},{'name':'Guadeloupe','value':'Guadeloupe'},{'name':'Guam','value':'Guam'},{'name':'Guatemala','value':'Guatemala'},{'name':'Guinea','value':'Guinea'},{'name':'Guinea-bissau','value':'Guinea-bissau'},{'name':'Guyana','value':'Guyana'},{'name':'Haiti','value':'Haiti'},{'name':'Heard Island And Mcdonald Islands','value':'Heard Island And Mcdonald Islands'},{'name':'Holy See (vatican City State)','value':'Holy See (vatican City State)'},{'name':'Honduras','value':'Honduras'},{'name':'Hong Kong','value':'Hong Kong'},{'name':'Hungary','value':'Hungary'},{'name':'Iceland','value':'Iceland'},{'name':'India','value':'India'},{'name':'Indonesia','value':'Indonesia'},{'name':'Iran, Islamic Republic Of','value':'Iran, Islamic Republic Of'},{'name':'Iraq','value':'Iraq'},{'name':'Ireland','value':'Ireland'},{'name':'Israel','value':'Israel'},{'name':'Italy','value':'Italy'},{'name':'Jamaica','value':'Jamaica'},{'name':'Japan','value':'Japan'},{'name':'Jordan','value':'Jordan'},{'name':'Kazakstan','value':'Kazakstan'},{'name':'Kenya','value':'Kenya'},{'name':'Kiribati','value':'Kiribati'},{'name':'Korea, Democratic People\'s Republic Of','value':'Korea, Democratic People\'s Republic Of'},{'name':'Korea, Republic Of','value':'Korea, Republic Of'},{'name':'Kosovo','value':'Kosovo'},{'name':'Kuwait','value':'Kuwait'},{'name':'Kyrgyzstan','value':'Kyrgyzstan'},{'name':'Lao People\'s Democratic Republic','value':'Lao People\'s Democratic Republic'},{'name':'Latvia','value':'Latvia'},{'name':'Lebanon','value':'Lebanon'},{'name':'Lesotho','value':'Lesotho'},{'name':'Liberia','value':'Liberia'},{'name':'Libyan Arab Jamahiriya','value':'Libyan Arab Jamahiriya'},{'name':'Liechtenstein','value':'Liechtenstein'},{'name':'Lithuania','value':'Lithuania'},{'name':'Luxembourg','value':'Luxembourg'},{'name':'Macau','value':'Macau'},{'name':'Macedonia, The Former Yugoslav Republic Of','value':'Macedonia, The Former Yugoslav Republic Of'},{'name':'Madagascar','value':'Madagascar'},{'name':'Malawi','value':'Malawi'},{'name':'Malaysia','value':'Malaysia'},{'name':'Maldives','value':'Maldives'},{'name':'Mali','value':'Mali'},{'name':'Malta','value':'Malta'},{'name':'Marshall Islands','value':'Marshall Islands'},{'name':'Martinique','value':'Martinique'},{'name':'Mauritania','value':'Mauritania'},{'name':'Mauritius','value':'Mauritius'},{'name':'Mayotte','value':'Mayotte'},{'name':'Mexico','value':'Mexico'},{'name':'Micronesia, Federated States Of','value':'Micronesia, Federated States Of'},{'name':'Moldova, Republic Of','value':'Moldova, Republic Of'},{'name':'Monaco','value':'Monaco'},{'name':'Mongolia','value':'Mongolia'},{'name':'Montenegro','value':'Montenegro'},{'name':'Montserrat','value':'Montserrat'},{'name':'Morocco','value':'Morocco'},{'name':'Mozambique','value':'Mozambique'},{'name':'Myanmar','value':'Myanmar'},{'name':'Namibia','value':'Namibia'},{'name':'Nauru','value':'Nauru'},{'name':'Nepal','value':'Nepal'},{'name':'Netherlands','value':'Netherlands'},{'name':'Netherlands Antilles','value':'Netherlands Antilles'},{'name':'New Caledonia','value':'New Caledonia'},{'name':'New Zealand','value':'New Zealand'},{'name':'Nicaragua','value':'Nicaragua'},{'name':'Niger','value':'Niger'},{'name':'Nigeria','value':'Nigeria'},{'name':'Niue','value':'Niue'},{'name':'Norfolk Island','value':'Norfolk Island'},{'name':'Northern Mariana Islands','value':'Northern Mariana Islands'},{'name':'Norway','value':'Norway'},{'name':'Oman','value':'Oman'},{'name':'Pakistan','value':'Pakistan'},{'name':'Palau','value':'Palau'},{'name':'Palestinian Territory, Occupied','value':'Palestinian Territory, Occupied'},{'name':'Panama','value':'Panama'},{'name':'Papua New Guinea','value':'Papua New Guinea'},{'name':'Paraguay','value':'Paraguay'},{'name':'Peru','value':'Peru'},{'name':'Philippines','value':'Philippines'},{'name':'Pitcairn','value':'Pitcairn'},{'name':'Poland','value':'Poland'},{'name':'Portugal','value':'Portugal'},{'name':'Puerto Rico','value':'Puerto Rico'},{'name':'Qatar','value':'Qatar'},{'name':'Reunion','value':'Reunion'},{'name':'Romania','value':'Romania'},{'name':'Russian Federation','value':'Russian Federation'},{'name':'Rwanda','value':'Rwanda'},{'name':'Saint Helena','value':'Saint Helena'},{'name':'Saint Kitts And Nevis','value':'Saint Kitts And Nevis'},{'name':'Saint Lucia','value':'Saint Lucia'},{'name':'Saint Pierre And Miquelon','value':'Saint Pierre And Miquelon'},{'name':'Saint Vincent And The Grenadines','value':'Saint Vincent And The Grenadines'},{'name':'Samoa','value':'Samoa'},{'name':'San Marino','value':'San Marino'},{'name':'Sao Tome And Principe','value':'Sao Tome And Principe'},{'name':'Saudi Arabia','value':'Saudi Arabia'},{'name':'Senegal','value':'Senegal'},{'name':'Serbia','value':'Serbia'},{'name':'Seychelles','value':'Seychelles'},{'name':'Sierra Leone','value':'Sierra Leone'},{'name':'Singapore','value':'Singapore'},{'name':'Slovakia','value':'Slovakia'},{'name':'Slovenia','value':'Slovenia'},{'name':'Solomon Islands','value':'Solomon Islands'},{'name':'Somalia','value':'Somalia'},{'name':'South Africa','value':'South Africa'},{'name':'South Georgia And The South Sandwich Islands','value':'South Georgia And The South Sandwich Islands'},{'name':'Spain','value':'Spain'},{'name':'Sri Lanka','value':'Sri Lanka'},{'name':'Sudan','value':'Sudan'},{'name':'Suriname','value':'Suriname'},{'name':'Svalbard And Jan Mayen','value':'Svalbard And Jan Mayen'},{'name':'Swaziland','value':'Swaziland'},{'name':'Sweden','value':'Sweden'},{'name':'Switzerland','value':'Switzerland'},{'name':'Syrian Arab Republic','value':'Syrian Arab Republic'},{'name':'Taiwan, Province Of China','value':'Taiwan, Province Of China'},{'name':'Tajikistan','value':'Tajikistan'},{'name':'Tanzania, United Republic Of','value':'Tanzania, United Republic Of'},{'name':'Thailand','value':'Thailand'},{'name':'Togo','value':'Togo'},{'name':'Tokelau','value':'Tokelau'},{'name':'Tonga','value':'Tonga'},{'name':'Trinidad And Tobago','value':'Trinidad And Tobago'},{'name':'Tunisia','value':'Tunisia'},{'name':'Turkey','value':'Turkey'},{'name':'Turkmenistan','value':'Turkmenistan'},{'name':'Turks And Caicos Islands','value':'Turks And Caicos Islands'},{'name':'Tuvalu','value':'Tuvalu'},{'name':'Uganda','value':'Uganda'},{'name':'Ukraine','value':'Ukraine'},{'name':'United Arab Emirates','value':'United Arab Emirates'},{'name':'United Kingdom','value':'United Kingdom'},{'name':'United States','value':'United States'},{'name':'United States Minor Outlying Islands','value':'United States Minor Outlying Islands'},{'name':'Uruguay','value':'Uruguay'},{'name':'Uzbekistan','value':'Uzbekistan'},{'name':'Vanuatu','value':'Vanuatu'},{'name':'Venezuela','value':'Venezuela'},{'name':'Viet Nam','value':'Viet Nam'},{'name':'Virgin Islands, British','value':'Virgin Islands, British'},{'name':'Virgin Islands, U.s.','value':'Virgin Islands, U.s.'},{'name':'Wallis And Futuna','value':'Wallis And Futuna'},{'name':'Western Sahara','value':'Western Sahara'},{'name':'Yemen','value':'Yemen'},{'name':'Zambia','value':'Zambia'},{'name':'Zimbabwe','value':'Zimbabwe'}]},{'n':'company','r':false,'t':'string','p':'oracle'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]},{'n':'seniority','r':false,'t':'options','p':'','o':[{'name':'Apprentice','value':'Apprentice'},{'name':'Director','value':'Director'},{'name':'Executive','value':'Executive'},{'name':'Intermediate','value':'Intermediate'},{'name':'Manager','value':'Manager'}]},{'n':'email','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]},{'n':'phone','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]}]},{'k':'sendLinkedinMessage','d':'Send Message To Linkedin Profile','ed':'Send a custom private message to a connected Linkedin profile (supported uris: https://www.linkedin.com/in/USERID)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'message','r':true,'t':'string','p':'Hi {{first}}, would you like to be part of my network? Thanks'}]},{'k':'getPersonMultipleDecisionMakerBySearch','d':'Get Decision Makers By Search Engine','ed':'Discover up to ten decision makers using search engines (Bing) by company name and area (optional)','g':'company','p':[{'n':'company','r':true,'t':'string','p':'oracle'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Ecommerce','value':'Ecommerce'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]},{'n':'clevel','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]},{'n':'location','r':false,'t':'string','p':''},{'n':'keyword','r':false,'t':'string','p':''}]},{'k':'getCompanyNameByDomain','d':'Get Company\'s Name By Domain','ed':'Get company name by company domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getCompanyPhoneByDomain','d':'Get Phone By Company Domain','ed':'Get company phone by company domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getProfileTwitterByCompany','d':'Get Twitter URI By Company','ed':'Get Twitter company profile by name without manual search on Google or Twitter.

This tool uses search engines (Bing and Google) through proxies','g':'company','p':[{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getDistanceByAddresses','d':'Get Distance Between Addresses','ed':'Returns straight-line distance in kilometers between two addresses','g':'geographic','p':[{'n':'address1','r':true,'t':'string','p':'Mallorca 120, 08036, Barcelona'},{'n':'address2','r':true,'t':'string','p':'Mallorca 70, 08036, Barcelona'}]},{'k':'getRouteByAddresses','d':'Get Route Between Addresses','ed':'Returns driving routing time, distance, fuel consumption and cost between two addresses','g':'geographic','p':[{'n':'address1','r':true,'t':'string','p':'Mallorca 120, 08036, Barcelona'},{'n':'address2','r':true,'t':'string','p':'Mallorca 70, 08036, Barcelona'},{'n':'fuel_consumption','r':true,'t':'string','p':'5'},{'n':'price_liter','r':true,'t':'string','p':'1.2'}]},{'k':'getDistanceByCoordinates','d':'Get Distance Between Coordinates','ed':'Returns straight-line distance in kilometers between two GPS coordinates (latitude and longitude)','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.30'}]},{'k':'getRouteByCoordinates','d':'Get Route Between Coordinates','ed':'Returns driving routing time, distance, fuel consumption and cost between two GPS coordinates (latitude and longitude)','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.30'}]},{'k':'getCoordinateByIp','d':'Get Coordinates By IP','ed':'Discover latitude and longitude coordinates of an IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getDistanceByIps','d':'Get Distance Between IPs','ed':'Returns straight-line distance in kilometers between two IP addresses','g':'geographic','p':[{'n':'ip1','r':true,'t':'string','p':'95.23.100.79'},{'n':'ip2','r':true,'t':'string','p':'88.190.16.36'}]},{'k':'getRouteByIps','d':'Get Route Between IPs','ed':'Returns driving routing time, distance, fuel consumption and cost between two IP addresses','g':'geographic','p':[{'n':'ip1','r':true,'t':'string','p':'95.23.100.79'},{'n':'ip2','r':true,'t':'string','p':'88.190.16.36'},{'n':'fuel_consumption','r':true,'t':'string','p':'5'},{'n':'price_liter','r':true,'t':'string','p':'1.2'}]},{'k':'getDistanceByPhones','d':'Get Distance Between Phones','ed':'Returns straight-line distance in kilometers between two landline phones, using city and province of every phone','g':'geographic','p':[{'n':'phone1','r':true,'t':'string','p':'932187670'},{'n':'phone2','r':true,'t':'string','p':'91213111111'}]},{'k':'getRouteByPhones','d':'Get Route Between Phones','ed':'Returns driving routing time, distance, fuel consumption and cost between two landline phones, using city and province of every phone (only Spain)','g':'geographic','p':[{'n':'phone1','r':true,'t':'string','p':'932187670'},{'n':'phone2','r':true,'t':'string','p':'91213111111'}]},{'k':'getDistanceByZipcodes','d':'Get Distance Between Zipcodes','ed':'Returns straight-line distance in kilometers between two zipcodes, using city and province of every zipcode','g':'geographic','p':[{'n':'zipcode1','r':true,'t':'string','p':'08012'},{'n':'zipcode2','r':true,'t':'string','p':'28080'}]},{'k':'getRouteByZipcodes','d':'Get Route Between Zipcodes','ed':'Returns driving routing time, distance, fuel consumption and cost between two zipcodes, using city and province of every zipcode','g':'geographic','p':[{'n':'zipcode1','r':true,'t':'string','p':'08012'},{'n':'zipcode2','r':true,'t':'string','p':'28080'}]},{'k':'getCoordinateCartesian','d':'Get Cartesian Coordinates','ed':'Get Cartesian coordinates (X,Y,Z/WGS84) by Latitude and Longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'getCoordinateDecimal','d':'Get Decimal Coordinates','ed':'Get Decimal coordinates (degrees, minutes and seconds) by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'checkDistanceEq','d':'Check Distance Is Equal','ed':'Discover if the distance between two coordinates is equal to another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'33.44'}]},{'k':'checkDistanceGe','d':'Check Distance Is Greater Or Equal','ed':'Discover if the distance in quilometers between two coordinates is greater or equal than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'33'}]},{'k':'checkDistanceGt','d':'Check Distance Is Greater','ed':'Discover if the distance in quilometers between two coordinates is greater than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'30'}]},{'k':'checkDistanceLe','d':'Check Distance Is Lower Or Equal','ed':'Discover if the distance in quilometers between two coordinates is lower or equal than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'34'}]},{'k':'checkDistanceLt','d':'Check Distance Is Lower','ed':'Discover if the distance in quilometers between two coordinates is lower than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'50'}]},{'k':'getCoordinateUsng','d':'Get USNG Coordinates','ed':'Get USNG coordinates by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'getCoordinateUtm','d':'Get UTM Coordinates','ed':'Get UTM coordinates by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'getCountryByCode','d':'Get Country By ISO Code','ed':'Get country name by its ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getCountryByCurrencyCode','d':'Get Country By Currency','ed':'Get country name by currency ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'getCountryByIp','d':'Get Country By IP','ed':'Get country name by IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCountryByName','d':'Get Country By Name','ed':'Get country by prefix','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'Spa'}]},{'k':'getCountryByPhone','d':'Get Country By Phone','ed':'Get country name by phone number, with worldwide coverage','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'57122000111'}]},{'k':'getCountryCodeByName','d':'Get Country Code By Name','ed':'Get Alpha2 code by country prefix or name','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'Spain'}]},{'k':'getCountryListByCode','d':'Get Countries By ISO Code','ed':'Get multiple countries by ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getCountryListByCurrencyCode','d':'Get Countries By Currency','ed':'Get multiple country names by currency ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'getCountryListByName','d':'Get Countries By Prefix','ed':'Get multiple country names by initial name','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'S'}]},{'k':'getCountryNormalized','d':'Get Normalized Country','ed':'Allow to normalize a country, removing non allowed characters','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'Spa1n'}]},{'k':'checkCountryValidIso','d':'Check Country Code Exists','ed':'Check if country ISO code exists','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getCurrencyByCountry','d':'Get Currency Code By Country Name','ed':'Get ISO currency code by a country name','g':'finance','p':[{'n':'country','r':true,'t':'string','p':'Spain'}]},{'k':'getCurrencyByCountryIsocode','d':'Get Currency Code By Country Code','ed':'Get ISO currency code by an ISO country code','g':'finance','p':[{'n':'country_code','r':true,'t':'string','p':'ES'}]},{'k':'getCurrencyByIp','d':'Get Currency By IP','ed':'Get ISO currency code by IP address','g':'finance','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCurrencyByIsocode','d':'Get Currency By ISO Currency Code','ed':'Get an ISO currency code by a currency ISO code','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'getCurrencyConvertedBetweenIsocodeDate','d':'Get Conversion By Currencies And Date','ed':'Convert amount between supported currencies and an exchange date','g':'finance','p':[{'n':'amount','r':true,'t':'string','p':'10'},{'n':'isocode1','r':true,'t':'options','p':'EUR','o':[{'name':'AUD','value':'AUD'},{'name':'BGN','value':'BGN'},{'name':'BRL','value':'BRL'},{'name':'CAD','value':'CAD'},{'name':'CHF','value':'CHF'},{'name':'CNY','value':'CNY'},{'name':'CZK','value':'CZK'},{'name':'DKK','value':'DKK'},{'name':'EUR','value':'EUR'},{'name':'GBP','value':'GBP'},{'name':'HKD','value':'HKD'},{'name':'HRK','value':'HRK'},{'name':'HUF','value':'HUF'},{'name':'IDR','value':'IDR'},{'name':'ILS','value':'ILS'},{'name':'INR','value':'INR'},{'name':'ISK','value':'ISK'},{'name':'JPY','value':'JPY'},{'name':'KRW','value':'KRW'},{'name':'MXN','value':'MXN'},{'name':'MYR','value':'MYR'},{'name':'NOK','value':'NOK'},{'name':'NZD','value':'NZD'},{'name':'PHP','value':'PHP'},{'name':'PLN','value':'PLN'},{'name':'RON','value':'RON'},{'name':'RUB','value':'RUB'},{'name':'SEK','value':'SEK'},{'name':'SGD','value':'SGD'},{'name':'THB','value':'THB'},{'name':'TRY','value':'TRY'},{'name':'USD','value':'USD'},{'name':'ZAR','value':'ZAR'}]},{'n':'isocode2','r':true,'t':'options','p':'USD','o':[{'name':'AUD','value':'AUD'},{'name':'BGN','value':'BGN'},{'name':'BRL','value':'BRL'},{'name':'CAD','value':'CAD'},{'name':'CHF','value':'CHF'},{'name':'CNY','value':'CNY'},{'name':'CZK','value':'CZK'},{'name':'DKK','value':'DKK'},{'name':'EUR','value':'EUR'},{'name':'GBP','value':'GBP'},{'name':'HKD','value':'HKD'},{'name':'HRK','value':'HRK'},{'name':'HUF','value':'HUF'},{'name':'IDR','value':'IDR'},{'name':'ILS','value':'ILS'},{'name':'INR','value':'INR'},{'name':'ISK','value':'ISK'},{'name':'JPY','value':'JPY'},{'name':'KRW','value':'KRW'},{'name':'MXN','value':'MXN'},{'name':'MYR','value':'MYR'},{'name':'NOK','value':'NOK'},{'name':'NZD','value':'NZD'},{'name':'PHP','value':'PHP'},{'name':'PLN','value':'PLN'},{'name':'RON','value':'RON'},{'name':'RUB','value':'RUB'},{'name':'SEK','value':'SEK'},{'name':'SGD','value':'SGD'},{'name':'THB','value':'THB'},{'name':'TRY','value':'TRY'},{'name':'USD','value':'USD'},{'name':'ZAR','value':'ZAR'}]},{'n':'date','r':false,'t':'string','p':'2018-02-12'}]},{'k':'getCurrencyListByCountry','d':'Get Currencies By Country','ed':'Get multiple ISO currency codes by a country name','g':'finance','p':[{'n':'country','r':true,'t':'string','p':'Spain'}]},{'k':'getCurrencyListByIp','d':'Get Currencies By IP','ed':'Get all ISO currency codes by an IP address','g':'finance','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCurrencyListByIsocode','d':'Get Currencies By ISO Code','ed':'Get multiple ISO currency codes by a country ISO code','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'checkCurrencyValidIso','d':'Check ISO Currency Code Is Valid','ed':'Discover if an ISO currency code is valid','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'checkDateBetw','d':'Check Date Is Between Dates','ed':'Discover if a date (date1) is betwen two dates (date2, date3)','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-20'},{'n':'date2','r':true,'t':'string','p':'1975-05-19'},{'n':'date3','r':true,'t':'string','p':'1975-05-22'}]},{'k':'getDateDifference','d':'Get Difference Between Dates','ed':'Returns difference between two dates (start and end) in seconds, minutes, hours or days
By default, difference is returned in seconds.','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'2018-10-10 00:00:00'},{'n':'date2','r':true,'t':'string','p':'2018-10-11 00:00:00'},{'n':'period','r':true,'t':'options','p':'seconds','o':[{'name':'Days','value':'days'},{'name':'Hours','value':'hours'},{'name':'Minutes','value':'minutes'},{'name':'Seconds','value':'seconds'}]}]},{'k':'checkDateEq','d':'Check Dates Are Equal','ed':'Discover if two dates are equal','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-20'},{'n':'date2','r':false,'t':'string','p':'1975-05-20'}]},{'k':'checkDateGe','d':'Check Date Is Greater Or Equal','ed':'Discover if a date is greater or equal','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-22'},{'n':'date2','r':false,'t':'string','p':'1975-05-21'}]},{'k':'checkDateGt','d':'Check Date Is Greater','ed':'Discover if a date is greater','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-22'},{'n':'date2','r':false,'t':'string','p':'1975-05-21'}]},{'k':'checkDateLe','d':'Check Date Is Lower Or Equal','ed':'Discover if a date is lower or equal','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-19'},{'n':'date2','r':false,'t':'string','p':'1975-05-20'}]},{'k':'checkDateLeap','d':'Check Date Is Leap Year','ed':'Discover if a date belongs to a leap year','g':'personal','p':[{'n':'date','r':false,'t':'string','p':'2008-05-20'}]},{'k':'checkDateLt','d':'Check Date Is Lower','ed':'Discover if a date is lower','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-20'},{'n':'date2','r':false,'t':'string','p':'1975-05-21'}]},{'k':'getDateNormalized','d':'Get Normalized Date','ed':'Allow to normalize a date, removing non allowed characters','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'20 /01/2018'}]},{'k':'getDateParsed','d':'Get Parsed Datetime','ed':'Parse datetime, without format dependency, into multiple fields','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'20/01/2018'}]},{'k':'checkDateValid','d':'Check Date Is Valid','ed':'Discover if a date has a valid format','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'getDeviceByUa','d':'Get Device By User Agent','ed':'Discover device features by user agent','g':'internet','p':[{'n':'useragent','r':true,'t':'string','p':'AppleTV5,3/9.1.1'}]},{'k':'getDomainBlacklists','d':'Get Domain Blacklists','ed':'Get all blacklists where a domain appears','g':'security','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getUrlByDomain','d':'Get Default URL By Domain','ed':'Get valid, existing and default URL when accessing a domain using a web browser.','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'getDomainByIp','d':'Get Domain By IP','ed':'Get the network name of and IP address','g':'internet','p':[{'n':'ip','r':true,'t':'string','p':'212.85.34.20'}]},{'k':'getDomainByUrl','d':'Get Root Domain By Web Address','ed':'Get root domain of any web address, removing non needed characters.','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.uproc.io'}]},{'k':'checkDomainCatchall','d':'Check Email Domain Is Catchall','ed':'Check if domain accepts all emails, existing or not','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'abinitio.es'}]},{'k':'checkDomainCertificate','d':'Check Domain Has Valid Certificate','ed':'Check if domain has a valid SSL certificate','g':'internet','p':[{'n':'domain','r':false,'t':'string','p':'uproc.io'}]},{'k':'getDomainCertificate','d':'Get Certificate By Domain','ed':'Get full SSL certificate data by domain (or website) and monitor your certificate status.

If domain has port 443 opened, a response will be returned.','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'checkDomainDisposable','d':'Check Email Domain Is Temporary','ed':'Check if domain is temporary or not','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'cowstore.org'}]},{'k':'checkDomainExist','d':'Check Domain Exists','ed':'Check if domain exists','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'mydomain.com'}]},{'k':'checkDomainFormat','d':'Check Domain Has Valid Format','ed':'Check if domain has a valid format','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'checkDomainFree','d':'Check Email Domain Is Free','ed':'Check if domain is a free service domain provider','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getDomainIsp','d':'Get ISP By Domain','ed':'Get ISP known name of email domain name (hotmail, yahoo, gmail, mailgun, zoho, other)','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getDomainLogo','d':'Get Logo By Domain','ed':'Discover logo (favicon) used in domain','g':'image','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'checkDomainMx','d':'Check Domain Has MX Record','ed':'Check if domain has a MX record','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'getUrlPdf','d':'Get Pdf By URL','ed':'Generate a PDF file by URL provided using Chrome browser','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'}]},{'k':'checkDomainRecord','d':'Check Domain Has DNS Record','ed':'Check if domain has a record of that type','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'mydomain.com'},{'n':'type','r':false,'t':'options','p':'A','o':[{'name':'A','value':'A'},{'name':'AAAA','value':'AAAA'},{'name':'CNAME','value':'CNAME'},{'name':'MX','value':'MX'},{'name':'NS','value':'NS'},{'name':'TXT','value':'TXT'}]}]},{'k':'getDomainRecord','d':'Get Domain Record By DNS Type','ed':'Get the domain record by its type','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'},{'n':'type','r':false,'t':'options','p':'A','o':[{'name':'A','value':'A'},{'name':'AAAA','value':'AAAA'},{'name':'CNAME','value':'CNAME'},{'name':'MX','value':'MX'},{'name':'NS','value':'NS'},{'name':'TXT','value':'TXT'}]}]},{'k':'getDomainRecords','d':'Get Domain DNS Records','ed':'Get all domain dns records','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'checkDomainReverse','d':'Check Domain Has IP','ed':'Check if domain has assigned the IP address defined','g':'internet','p':[{'n':'domain','r':false,'t':'string','p':'mail.nova.es'},{'n':'ip','r':true,'t':'string','p':'212.85.34.20'}]},{'k':'getDomainReverseIp','d':'Get IP By Domain','ed':'Get the IPv4 address linked with a domain','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'www.gmail.com'}]},{'k':'getUrlScreenshot','d':'Get Screenshot By URL','ed':'Generate a screenshot by URL provided using Chrome browser','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'},{'n':'width','r':false,'t':'options','p':'640','o':[{'name':'1024','value':'1024'},{'name':'160','value':'160'},{'name':'320','value':'320'},{'name':'640','value':'640'},{'name':'800','value':'800'}]},{'n':'fullpage','r':false,'t':'options','p':'no','o':[{'name':'No','value':'no'},{'name':'Yes','value':'yes'}]}]},{'k':'getUrlShareableLinks','d':'Get Shareable Links','ed':'Generates shareable URIs to use on social networks and email using a content URI and a text.','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.uproc.io/'},{'n':'text','r':true,'t':'string','p':'This is an amazing title for my content'}]},{'k':'getDomainTechnologies','d':'Get Technologies By Domain','ed':'Discover client and server technologies used in domain','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'getUrlTechnologies','d':'Get Technologies By URL','ed':'Discover client and server technologies used in web page','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.uproc.io/'}]},{'k':'getDomainVisits','d':'Get Visits By Domain','ed':'Get Website visits and rank of any domain','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getDomainWhois','d':'Get Whois By Domain','ed':'Get the domain whois data by fields','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'}]},{'k':'getIpWhois','d':'Get Whois By IP Address','ed':'Get whois data fields by IP address provided.','g':'internet','p':[{'n':'ip','r':true,'t':'string','p':'140.82.118.4'}]},{'k':'sendEmailCustom','d':'Send Custom Email','ed':'Send a custom email (HTML supported) to a recipient','g':'communication','p':[{'n':'email_from','r':true,'t':'string','p':'mcolomer@killia.com'},{'n':'email_to','r':true,'t':'string','p':'mcolomer@gmail.com'},{'n':'subject','r':true,'t':'string','p':'Welcome email'},{'n':'body','r':true,'t':'string','p':'Hi!

Welcome to uProc and start improving your business processes!'}]},{'k':'checkEmailDisposable','d':'Check Email Is Disposable Domain','ed':'Check if email domain belongs to a disposable email service','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'info@jetable.com'}]},{'k':'getEmailDomain','d':'Get Domain By Email','ed':'Allow to get domain from an email','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkEmailExists','d':'Check Email Exists (Simple)','ed':'Discover if the email recipient exists, returning email status','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkEmailExistsExtended','d':'Check Email Exists (Extended)','ed':'Discover if an email is valid, hardbounce, softbounce, spamtrap, free, temporary and recipient exists.

There are catchall (like Yahoo) or temporary domains that do not return the actual existence of an email','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getEmailFirstReferences','d':'Get First Web References Of An Email','ed':'Get three first web references of an email published on Internet','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'mcolomer@killia.com'}]},{'k':'getEmailFix','d':'Get Fixed Domain Email','ed':'Allows you to fix the email domain of those misspelled emails (supports all domains)','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test@gmil.com'}]},{'k':'checkEmailFormat','d':'Check Email Has Valid Format','ed':'Check if email has a valid format','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test@test.com'}]},{'k':'checkEmailFree','d':'Check Email Is Free','ed':'Check if email belongs to free service provider, like gmail, hotmail, ...','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'info@gmail.com'}]},{'k':'getEmailGdprListByDomain','d':'Get GDPR Emails By Domain','ed':'Get GDPR compliant emails list by domain for your Email Marketing campaigns in Europe.','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'getEmailListByDomain','d':'Get Emails By Domain','ed':'Get emails list found on internet by domain or URI (similar to hunter.io)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'},{'n':'page','r':false,'t':'number','p':'1'}]},{'k':'getEmailListByEmail','d':'Get Emails By Email','ed':'Get emails list found on internet by non-free email (similar to hunter.io)','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'},{'n':'page','r':false,'t':'number','p':'1'}]},{'k':'getEmailListInSite','d':'Get Emails In Website','ed':'Get emails list found inside website by domain or URI (similar to hunter.io)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'uoc.edu'},{'n':'page','r':false,'t':'number','p':'1'}]},{'k':'getEmailNormalized','d':'Get Normalized Email','ed':'Allow to normalize email address, removing non allowed characters','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test @gmail.com'}]},{'k':'getEmailRecipient','d':'Get Email By Name, Surname And Domain','ed':'Discover an email by company website or domain and prospect\'s firstname and lastname.

If \'verify\' method is selected, tool checks multiple email variants in real-time, and returns a result depending on email server response','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'},{'n':'firstname','r':true,'t':'string','p':'Miquel'},{'n':'lastname','r':false,'t':'string','p':'Colomer'},{'n':'mode','r':true,'t':'options','p':'guess','o':[{'name':'Guess','value':'guess'},{'name':'Verify','value':'verify'}]}]},{'k':'getEmailRecipientGdpr','d':'Get Public Email By Name, Surname And Domain (GDPR)','ed':'Discover an email by company website or domain and prospect\'s firstname and lastname.

The tool only uses publicly available emails found on the internet and matches the recipient by first name and last name (GDPR compliant)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'},{'n':'firstname','r':true,'t':'string','p':'hello'},{'n':'lastname','r':false,'t':'string','p':''}]},{'k':'getEmailReferences','d':'Get Web References Of An Email','ed':'Get web references of an email published on Internet','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'mcolomer@killia.com'}]},{'k':'checkEmailRole','d':'Check Email Is Role Based','ed':'Check if email belongs to a system or role based account','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test@gmail.com'}]},{'k':'sendMobileSms','d':'Send Custom Sms','ed':'Send a custom sms to a recipient with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'},{'n':'text','r':true,'t':'string','p':'Thanks for your participation!'}]},{'k':'checkEmailSmtp','d':'Check Email Has SMTP Server','ed':'Check if email domain has an SMTP server to receive emails','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkEmailSpamtrap','d':'Check Email Is Spam Trap','ed':'Check if email is a spam trap','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'zzzwuzhdgvrxy@yahoo.co.jp'}]},{'k':'getEmailType','d':'Get Email Type','ed':'Checks if email is personal (miquel@uproc.io) or generic (hello@uproc.io).','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getFileCopiedBetweenUrls','d':'Get File Copied Between URLs','ed':'Copy file from one URL to another URL','g':'internet','p':[{'n':'source','r':true,'t':'string','p':'http://www.pdf995.com/samples/pdf.pdf'},{'n':'destination','r':true,'t':'string','p':'s3://ACCESSKEY:SECRETKEY@s3.amazon.com/BUCKET_NAME/sample.pdf'}]},{'k':'getFullnameParsed','d':'Get Parsed Fullname','ed':'Normalize fullname, fixing abbreviations, sorting if necessary and returning firstname, lastname and gender','g':'personal','p':[{'n':'fullname','r':true,'t':'string','p':'Colomer Salas Miquel'}]},{'k':'getGenderByEmail','d':'Get Gender By Email','ed':'Discover the gender of a person by the email, if person name is included in email','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel.colomer@gmail.com'}]},{'k':'getGenderByPersonalName','d':'Get Gender By Name','ed':'Discover the gender of a person or company by name','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Marc'}]},{'k':'checkGenderValid','d':'Check Gender Is Valid','ed':'Discover if a gender value is valid (multilanguage)','g':'personal','p':[{'n':'gender','r':true,'t':'string','p':'male'}]},{'k':'getImageExif','d':'Get EXIF Metadata From Image','ed':'It allows to discover all geograhical and technical EXIF metadata present in a photographic JPEG image.','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://killia-internal.s3-eu-west-1.amazonaws.com/sample/uproc_photo_with_exif.jpg'}]},{'k':'getImageWithText','d':'Get Image With Text','ed':'Generate a new image by URL and text','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://killia-internal.s3-eu-west-1.amazonaws.com/sample/uproc_sample_resized.jpg'},{'n':'text','r':true,'t':'string','p':'Hi Miquel!'},{'n':'size','r':false,'t':'string','p':'80'}]},{'k':'getQrDecoded','d':'Get Decoded QR Code','ed':'Get QR Code decoded content by an image URL','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://s3.amazonaws.com/any-file/qr_38efdf6c60074375a6b0061201c644ac.png'}]},{'k':'getQrEncoded','d':'Get Encoded QR Code','ed':'Get QR Code encoded by a text','g':'image','p':[{'n':'text','r':true,'t':'string','p':'Sample text to encode'}]},{'k':'getIpBlacklists','d':'Get Ip Blacklists','ed':'Get all blacklists where an IP address appears','g':'security','p':[{'n':'ip','r':true,'t':'string','p':'172.217.168.165'}]},{'k':'getLinkedinConnections','d':'Get LinkedIn Last Received Connections','ed':'Extract last 80 connections from your LinkedIn profile','g':'communication','p':[{'n':'list','r':false,'t':'string','p':'last-connections'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'}]}]},{'k':'getLinkedinGroupMembers','d':'Get LinkedIn Group Members','ed':'Get members in a LinkedIn group','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/groups/59923/members/'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getLinkedinInvitations','d':'Get LinkedIn Last Sent Invitations','ed':'Extract last 80 invitations sent from your LinkedIn','g':'communication','p':[{'n':'list','r':false,'t':'string','p':'last-invitations'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'}]}]},{'k':'getLinkedinPostComments','d':'Get LinkedIn Post Comments','ed':'Get users who comment a post on LinkedIn','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/posts/miquelcolomersalas_gdpr-emails-emailmarketing-activity-6607189465423314944-dbPv'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getLinkedinPostLikes','d':'Get LinkedIn Post Likes','ed':'Get users who like a post on LinkedIn','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/posts/miquelcolomersalas_gdpr-emails-emailmarketing-activity-6607189465423314944-dbPv'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getLinkedinProfile','d':'Get LinkedIn Profile','ed':'Extract a LinkedIn profile (url format accepted: https://linkedin.com/in/USERNAME)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'list','r':false,'t':'string','p':'my-list'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'},{'name':'Slow','value':'slow'}]}]},{'k':'checkLinkedinProfileIsContact','d':'Check LinkedIn Profile Is Contact','ed':'Check if a LinkedIn profile (url format accepted: https://linkedin.com/in/USERNAME) is a first degree contact','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getLinkedinProfiles','d':'Get LinkedIn Profiles','ed':'Extract results from a LinkedIn search (url format accepted: https://linkedin.com/search/results/people/)','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/search/results/people/?facetGeoRegion=%5B%22es%3A5064%22%5D&facetNetwork=%5B%22S%22%2C%22O%22%5D&keywords=cmo%20barcelona'},{'n':'list','r':false,'t':'string','p':'my-list'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'}]},{'n':'amount','r':false,'t':'string','p':'10'}]},{'k':'getLinkedinProfilesByCompany','d':'Get LinkedIn Company Employees','ed':'Extract results from a LinkedIn search (url format accepted: https://linkedin.com/search/results/people/)','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/company/ibm-aspera/'},{'n':'list','r':false,'t':'string','p':'employees'}]},{'k':'getLinkedinProfilesByContent','d':'Get LinkedIn Profiles By Content','ed':'Extract fastly last profiles that have published content on LinkedIn by specific keywords','g':'communication','p':[{'n':'keywords','r':false,'t':'string','p':'Growth'},{'n':'list','r':false,'t':'string','p':'content'}]},{'k':'sendLinkedinVisit','d':'Send LinkedIn Profile Visit','ed':'Visits a profile to show interest and get profile views in return from contact, increasing your LinkedIn network','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas/'}]},{'k':'checkListContains','d':'Check List Contains','ed':'Check if the list contains a specific item','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'text','r':true,'t':'string','p':'2'}]},{'k':'checkListEnds','d':'Check List Ends With','ed':'Check if the list ends with a specific element','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'text','r':true,'t':'string','p':'3'}]},{'k':'checkListLengthBetw','d':'Check Length List Between','ed':'Check if the length of a list is between two quantities','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length1','r':true,'t':'number','p':'3'},{'n':'length2','r':true,'t':'number','p':'4'}]},{'k':'checkListLengthEq','d':'Check Length List Equal','ed':'Checks if the length of a list equals a specified quantity','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkListLengthGe','d':'Check Length List Greater Or Equal','ed':'Check if the length of a list is greater than or equal to a certain amount','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkListLengthGt','d':'Check Length List Greater','ed':'Check if the length of a list is greater than a certain amount','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'2'}]},{'k':'checkListLengthLe','d':'Check Length List Lower Or Equal','ed':'Check if the length of a list is less than or equal to a certain amount','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkListLengthLt','d':'Check Length List Lower','ed':'','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkListMax','d':'Check Greater Element','ed':'Checks if the largest item in a list matches the provided item','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,4,8,3,4,5'},{'n':'number','r':true,'t':'string','p':'8'}]},{'k':'getListMax','d':'Get Greater Element','ed':'Returns the largest item in a list','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'checkListMin','d':'Check Lower Element','ed':'Checks if the smallest element in a list matches the provided element','g':'text','p':[{'n':'list','r':true,'t':'string','p':'5,6,1,3,7'},{'n':'number','r':true,'t':'string','p':'1'}]},{'k':'getListMin','d':'Get Lower Element','ed':'Returns the smallest item in a list','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'getListSort','d':'Get Sorted List','ed':'Returns an ascending sorted list','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'checkListSorted','d':'Check List Is Sorted','ed':'Check if a list is sorted in ascending order','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3,4,5'}]},{'k':'checkListStarts','d':'Check List Starts With','ed':'Check if the list starts with a specific element','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'text','r':true,'t':'string','p':'1'}]},{'k':'checkListUnique','d':'Check Unique Es List','ed':'Check if a list consists of unique elements','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3,4,5'}]},{'k':'getListUnique','d':'Get Unique List','ed':'Returns a single list, with no repeating elements','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'checkListValid','d':'Check Valid List','ed':'Check if the supplied values ​​form a valid list of elements','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'separator','r':true,'t':'string','p':','}]},{'k':'getLocaleByIp','d':'Get Locale Data By IP','ed':'Discover locale data (currency, language) by ipv4 or ipv6 address.','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationByCoordinates','d':'Get Location By Coordinates','ed':'Discover the city name, zipcode, province or country by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.619206,2.2920828'}]},{'k':'getLocationByIp','d':'Get Location By IP','ed':'Discover the city name, zipcode, province, country, latitude and longitude from an ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getReputationByIp','d':'Get Reputation By IP','ed':'Discover reputation by ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getTimeByIp','d':'Get Time Data By IP','ed':'Discover datetime data by ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationByName','d':'Get Location By Name','ed':'Discover location data by name','g':'geographic','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick S.L.'}]},{'k':'getLocationByPhone','d':'Get Location By Landline Phone (ES)','ed':'Discover the city and the province from a landline phone number (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'848491812'}]},{'k':'getLocationByZipcode','d':'Get Location By Zipcode (ES)','ed':'Discover the city and the province from a zipcode number (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getLocationExtendedByIp','d':'Get Extended Location By IP','ed':'Discover geographical, company, timezone and reputation data by IPv4 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationGeocodedByIp','d':'Get Geocoded Location By IP','ed':'Discover the city name, zipcode, province, country, latitude and longitude from an ipv4 or ipv6 address and geocodes it','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationListByName','d':'Get Locations By Name','ed':'Get most relevants locations by name (Google Maps typical search)','g':'geographic','p':[{'n':'name','r':true,'t':'string','p':'Pintores Granollers'}]},{'k':'getLocationListByParams','d':'Get Locations By Parameters','ed':'Get most relevants locations by name, category, location and radius','g':'geographic','p':[{'n':'name','r':false,'t':'string','p':'Saba'},{'n':'category','r':false,'t':'options','p':'parking','o':[{'name':'Accounting','value':'accounting'},{'name':'Airport','value':'airport'},{'name':'Amusement_park','value':'amusement_park'},{'name':'Aquarium','value':'aquarium'},{'name':'Art_gallery','value':'art_gallery'},{'name':'Atm','value':'atm'},{'name':'Bakery','value':'bakery'},{'name':'Bank','value':'bank'},{'name':'Bar','value':'bar'},{'name':'Beauty_salon','value':'beauty_salon'},{'name':'Bicycle_store','value':'bicycle_store'},{'name':'Book_store','value':'book_store'},{'name':'Bowling_alley','value':'bowling_alley'},{'name':'Bus_station','value':'bus_station'},{'name':'Cafe','value':'cafe'},{'name':'Campground','value':'campground'},{'name':'Car_dealer','value':'car_dealer'},{'name':'Car_rental','value':'car_rental'},{'name':'Car_repair','value':'car_repair'},{'name':'Car_wash','value':'car_wash'},{'name':'Casino','value':'casino'},{'name':'Cemetery','value':'cemetery'},{'name':'Church','value':'church'},{'name':'City_hall','value':'city_hall'},{'name':'Clothing_store','value':'clothing_store'},{'name':'Convenience_store','value':'convenience_store'},{'name':'Courthouse','value':'courthouse'},{'name':'Dentist','value':'dentist'},{'name':'Department_store','value':'department_store'},{'name':'Doctor','value':'doctor'},{'name':'Electrician','value':'electrician'},{'name':'Electronics_store','value':'electronics_store'},{'name':'Embassy','value':'embassy'},{'name':'Establishment','value':'establishment'},{'name':'Finance','value':'finance'},{'name':'Fire_station','value':'fire_station'},{'name':'Florist','value':'florist'},{'name':'Food','value':'food'},{'name':'Funeral_home','value':'funeral_home'},{'name':'Furniture_store','value':'furniture_store'},{'name':'Gas_station','value':'gas_station'},{'name':'General_contractor','value':'general_contractor'},{'name':'Grocery_or_supermarket','value':'grocery_or_supermarket'},{'name':'Gym','value':'gym'},{'name':'Hair_care','value':'hair_care'},{'name':'Hardware_store','value':'hardware_store'},{'name':'Health','value':'health'},{'name':'Hindu_temple','value':'hindu_temple'},{'name':'Home_goods_store','value':'home_goods_store'},{'name':'Hospital','value':'hospital'},{'name':'Insurance_agency','value':'insurance_agency'},{'name':'Jewelry_store','value':'jewelry_store'},{'name':'Laundry','value':'laundry'},{'name':'Lawyer','value':'lawyer'},{'name':'Library','value':'library'},{'name':'Liquor_store','value':'liquor_store'},{'name':'Local_government_office','value':'local_government_office'},{'name':'Locksmith','value':'locksmith'},{'name':'Lodging','value':'lodging'},{'name':'Meal_delivery','value':'meal_delivery'},{'name':'Meal_takeaway','value':'meal_takeaway'},{'name':'Mosque','value':'mosque'},{'name':'Movie_rental','value':'movie_rental'},{'name':'Movie_theater','value':'movie_theater'},{'name':'Moving_location','value':'moving_location'},{'name':'Museum','value':'museum'},{'name':'Night_club','value':'night_club'},{'name':'Painter','value':'painter'},{'name':'Park','value':'park'},{'name':'Parking','value':'parking'},{'name':'Pet_store','value':'pet_store'},{'name':'Pharmacy','value':'pharmacy'},{'name':'Physiotherapist','value':'physiotherapist'},{'name':'Place_of_worship','value':'place_of_worship'},{'name':'Plumber','value':'plumber'},{'name':'Police','value':'police'},{'name':'Post_office','value':'post_office'},{'name':'Real_estate_agency','value':'real_estate_agency'},{'name':'Restaurant','value':'restaurant'},{'name':'Roofing_contractor','value':'roofing_contractor'},{'name':'Rv_park','value':'rv_park'},{'name':'School','value':'school'},{'name':'Shoe_store','value':'shoe_store'},{'name':'Shopping_mall','value':'shopping_mall'},{'name':'Spa','value':'spa'},{'name':'Stadium','value':'stadium'},{'name':'Storage','value':'storage'},{'name':'Store','value':'store'},{'name':'Subway_station','value':'subway_station'},{'name':'Synagogue','value':'synagogue'},{'name':'Taxi_stand','value':'taxi_stand'},{'name':'Train_station','value':'train_station'},{'name':'Transit_station','value':'transit_station'},{'name':'Travel_agency','value':'travel_agency'},{'name':'University','value':'university'},{'name':'Veterinary_care','value':'veterinary_care'},{'name':'Zoo','value':'zoo'}]},{'n':'location','r':true,'t':'string','p':'41.3851,2.1734'},{'n':'radius','r':false,'t':'string','p':'250'}]},{'k':'checkMobileAlive','d':'Check Mobile Is Alive','ed':'Discover if a mobile number is switched on to call it later, with worldwide coverage.
Some carriers don\'t return if mobile is alive (like Vodafone)','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileCountryCode','d':'Get Country ISO Code By Mobile','ed':'Allow to get country code (two chars) of a mobile phone number with international format','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileCountryPrefix','d':'Get Phone Prefix By Country ISO Code','ed':'Allow to get country prefix number by country code (2 characters)','g':'communication','p':[{'n':'country','r':true,'t':'string','p':'ES'}]},{'k':'checkMobileExist','d':'Check Mobile Exists','ed':'Discover if mobile phone number exists in network operator, with worldwide coverage.
Get advanced mobile KPIs with "Mobile lookup" tool.','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileFormat','d':'Check Mobile Has Valid Format','ed':'Discover if mobile phone number has a valid format, with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34623123213'},{'n':'country','r':true,'t':'string','p':'ES'}]},{'k':'checkMobileFormatEs','d':'Check Mobile Has Valid Format (ES)','ed':'Discover if mobile phone number has a valid format (only Spain)','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'623123213'}]},{'k':'getMobileFormatted','d':'Get Formatted Mobile','ed':'Format international mobile number by country ISO code (2 letters).','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'},{'n':'country','r':false,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'getMobileHlrLookup','d':'Get HLR Mobile Lookup','ed':'Discover if mobile exist via real time [HLR](https://en.wikipedia.org/wiki/Home_location_register) query','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileImei','d':'Check Imei Is Valid','ed':'Discover if Imei number has a valid format','g':'communication','p':[{'n':'imei','r':true,'t':'string','p':'490154203237518'}]},{'k':'getMobileLookup','d':'Get Mobile Lookup','ed':'Discover if mobile exist via real time [HLR](https://en.wikipedia.org/wiki/Home_location_register) query, as well as portability and roaming data','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileMnpLookup','d':'Get Mobile Portability Lookup','ed':'Get existence, portability and roaming of a mobile phone, via [MNP](https://en.wikipedia.org/wiki/Mobile_number_portability) query','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileNormalized','d':'Get Normalized Mobile','ed':'Allow to normalize a mobile phone, removing non allowed characters','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34 62318 2 770'},{'n':'country','r':false,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'getMobileOrPhoneLookupEs','d':'Get Mobile/Landline Lookup (Spain)','ed':'Discover if mobile or landline prefix exists on Spain.','g':'communication','p':[{'n':'number','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileOrPhoneMnpEs','d':'Get Landline/Mobile Portability Lookup (ES)','ed':'Get portability data about a landline or mobile number, only for Spain','g':'communication','p':[{'n':'number','r':true,'t':'string','p':'605281220'}]},{'k':'checkMobileSms','d':'Check Mobile Supports Sms','ed':'Discover if a mobile number can receive sms, with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileValidPrefix','d':'Check Mobile Has Valid Prefix','ed':'Discover if mobile phone number has a valid prefix, with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileValidPrefixEs','d':'Check Mobile Has Valid Prefix (ES)','ed':'Discover if spanish mobile phone number has a valid prefix','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'605281220'}]},{'k':'getNameByPrefix','d':'Get Name By Prefix (ES)','ed':'Get first personal name matching by prefix and gender from INE data source (only Spain)','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Marce'},{'n':'gender','r':false,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]}]},{'k':'checkNameExist','d':'Check Name Exists (ES)','ed':'Check if a personal name exists in INE data source (only Spain)','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Juan'}]},{'k':'getNameListByPrefix','d':'Get Names By Prefix','ed':'Get multiple personal names by prefix','g':'geographic','p':[{'n':'name','r':true,'t':'string','p':'Marce'},{'n':'gender','r':false,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]}]},{'k':'getNameNormalized','d':'Get Normalized Name','ed':'Normalize name removing non allowed characters','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'M4rc'}]},{'k':'checkNameValid','d':'Check Name Has Valid Format','ed':'Check if name contains accepted characters','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Gonzalez'}]},{'k':'getUrlAnalysis','d':'Get URL Analysis','ed':'Analyze URL\'s health status about SSL, broken links, conflictive HTTP links with SSL, and more.','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'}]},{'k':'getNetAton','d':'Get Number By IP','ed':'Convert an IP address to numeric notation','g':'internet','p':[{'n':'ip','r':true,'t':'string','p':'62.12.22.11'}]},{'k':'getNetByIp','d':'Get Network By IP','ed':'Discover network data by ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'checkUrlContains','d':'Check URL Contains','ed':'Check if an URL contains string or regular expression (case insensitive)','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'},{'n':'regex','r':true,'t':'string','p':'uProc'}]},{'k':'getUrlContents','d':'Get Contents From URL','ed':'Get text data from web, pdf or image (png, jpg, gif), allowing to filter some elements by regular expressions or field names (email, phone, zipcode).

Learn about regular expressions on [Wikipedia](https://en.wikipedia.org/wiki/Regular_expression)','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://docs.uproc.io/pdf/resumen_del_servicio_EN.pdf'},{'n':'selector','r':false,'t':'string','p':'emails'}]},{'k':'getUrlContentsParsed','d':'Get Parsed Contents From URL','ed':'Obtains the content of a web in a structured way in JSON format to be able to save it wherever you want','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.bing.com/search?q=killia+technologies'}]},{'k':'getUrlDecode','d':'Get Decoded URL','ed':'Decode URL to recover original','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https%3A%2F%2Fgoogle.es'}]},{'k':'getUrlEncode','d':'Get Encoded URL','ed':'Encode URL to avoid problems','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'http://google.com'}]},{'k':'checkUrlExist','d':'Check URL Exists','ed':'Check if an URL exists','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'http://www.google.com'}]},{'k':'getNetFixip','d':'Get Fixed IP','ed':'Fix an IP address to the right format','g':'internet','p':[{'n':'number','r':true,'t':'string','p':'212169160147'}]},{'k':'checkNetHostAlive','d':'Check Host Is Up','ed':'Discover if a computer is switched on','g':'internet','p':[{'n':'host','r':true,'t':'string','p':'www.google.es'}]},{'k':'checkStringIp','d':'Check IP Has Valid Format','ed':'Check if IPv4 or IPv6 address has a valid format','g':'text','p':[{'n':'ip','r':true,'t':'string','p':'23.45.57.123'}]},{'k':'checkStringIp4','d':'Check IPv4 Has Valid Format','ed':'Check if IPv4 address has a valid format','g':'text','p':[{'n':'ip','r':true,'t':'string','p':'127.0.0.1'}]},{'k':'checkStringIp6','d':'Check IPv6 Has Valid Format','ed':'Check if IPv6 address has a valid format','g':'text','p':[{'n':'ip','r':true,'t':'string','p':'2a01:c50e:3544:bd00:4df0:7609:251a:f6d0'}]},{'k':'getUrlListContentsParsed','d':'Get Parsed Contents From Results URL','ed':'Obtains a list with multiple results from a website in a structured way in JSON format to be able to save it wherever you want','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.bing.com/search?q=killia+technologies'}]},{'k':'getNetNtoa','d':'Get IP By Number','ed':'Convert a number to an IP address','g':'internet','p':[{'n':'number','r':true,'t':'string','p':'1501706957'}]},{'k':'getUrlParsed','d':'Get Parsed URL','ed':'Decode URL into multiple fields','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://docs.uproc.io/pdf/resumen_del_servicio_EN.pdf'}]},{'k':'getNetScan','d':'Get Opened Ports In Host','ed':'Scan a host and returns most common open ports: 21, 22, 23, 25, 53, 80, 110, 143, 443, 3306, 27017','g':'internet','p':[{'n':'host','r':true,'t':'string','p':'google.es'}]},{'k':'checkNetServiceUp','d':'Check Service Is Up','ed':'Discover if a service in a port is available','g':'internet','p':[{'n':'host','r':true,'t':'string','p':'www.google.com'},{'n':'port','r':true,'t':'string','p':'80'}]},{'k':'getUrlTables','d':'Get Table From URL','ed':'Get data from existing table in HTML page (by table number) or in a PDF file (by table column number) in CSV format (columns delimited by ;)','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2'},{'n':'table','r':false,'t':'string','p':'3'}]},{'k':'checkUrlValid','d':'Check URL Is Valid','ed':'Check that an URL has a valid format','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'http://www.google.com'}]},{'k':'checkNumberBetw','d':'Check Number Is Between','ed':'Check if number is between two values','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'21'},{'n':'number3','r':true,'t':'string','p':'24'}]},{'k':'checkNumberDecimal','d':'Check Decimal Number Is Valid','ed':'Check if value is a decimal number','p':[{'n':'number','r':true,'t':'string','p':'0.23'}]},{'k':'checkNumberEq','d':'Check Number Is Equal','ed':'Check if number is equal to another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberEven','d':'Check Even Number Is Valid','ed':'Check if number is even','p':[{'n':'number','r':true,'t':'string','p':'2'}]},{'k':'checkNumberGe','d':'Check Number Is Greater Or Equal','ed':'Check if number is greater or equal than another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberGt','d':'Check Number Is Greater','ed':'Check if number is greater than another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'22'}]},{'k':'checkNumberLe','d':'Check Number Is Lower Or Equal','ed':'Check if number is lower or equal than another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberLt','d':'Check Number Is Lower','ed':'Check if number is lower than another','p':[{'n':'number1','r':true,'t':'string','p':'22'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberLuhn','d':'Check Luhn Number Is Valid','ed':'Check if it a valid Luhn number','g':'security','p':[{'n':'luhn','r':true,'t':'string','p':'79927398713'}]},{'k':'checkNumberMod','d':'Check Modulus Is Equals','ed':'Check if modulus between two numbers is equal to a value','p':[{'n':'number','r':true,'t':'string','p':'10'},{'n':'mod','r':true,'t':'string','p':'2'},{'n':'rest','r':true,'t':'string','p':'0'}]},{'k':'checkNumberNatural','d':'Check Natural Number Is Valid','ed':'Check if value is a natural number','p':[{'n':'number','r':true,'t':'string','p':'0'}]},{'k':'checkStringNumeric','d':'Check Numeric String Is Valid','ed':'Check if string length contains only numbers','g':'text','p':[{'n':'text','r':true,'t':'string','p':'123'}]},{'k':'checkNumberOdd','d':'Check Odd Number Is Valid','ed':'Check if number is odd','p':[{'n':'number','r':true,'t':'string','p':'3'}]},{'k':'checkNumberPrime','d':'Check Prime Number Is Valid','ed':'Check if number is prime','p':[{'n':'number','r':true,'t':'string','p':'11'}]},{'k':'checkPasswordStrong','d':'Check Password Is Strong','ed':'Check is password is sure and contains a lowercase, uppercase, numbers, special characters and have a minimum length of four characters','g':'security','p':[{'n':'password','r':true,'t':'string','p':'1agdA*$#'}]},{'k':'getPersonByEmail','d':'Get Person By Email','ed':'Get personal data by email','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getPersonByFirstnameLastnameCompanyLocation','d':'Get Person By Firstname, Lastname, Company And Location','ed':'Get personal data by firstname, lastname, company and location','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'Miquel'},{'n':'lastname','r':true,'t':'string','p':'Colomer'},{'n':'company','r':false,'t':'string','p':'uProc'},{'n':'location','r':false,'t':'string','p':''}]},{'k':'getPersonByMobile','d':'Get Person By Mobile','ed':'Get personal data by mobile','g':'personal','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'sendPersonEmailToList','d':'Send Email\'s Contact To List','ed':'Add a contact email to a person list','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getPersonExtendedByEmail','d':'Get Person (Extended) By Email','ed':'Get prospect\'s contact data and company\'s location and social data by email','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getPersonExtendedByEmailAndCompany','d':'Get Person (Extended) By Email And Company','ed':'Get contact, location and social data by email and company name and location','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'},{'n':'company','r':false,'t':'string','p':'uProc'}]},{'k':'getPersonFakedData','d':'Get Random Person Data','ed':'Generates random fake data','g':'personal','p':[{'n':'locality','r':true,'t':'options','p':'English','o':[{'name':'Australia (English)','value':'Australia (English)'},{'name':'Australia Ocker (English)','value':'Australia Ocker (English)'},{'name':'Azerbaijani','value':'Azerbaijani'},{'name':'Bork (English)','value':'Bork (English)'},{'name':'Canada (English)','value':'Canada (English)'},{'name':'Canada (French)','value':'Canada (French)'},{'name':'Chinese','value':'Chinese'},{'name':'Chinese (Taiwan)','value':'Chinese (Taiwan)'},{'name':'Czech','value':'Czech'},{'name':'Dutch','value':'Dutch'},{'name':'English','value':'English'},{'name':'Farsi','value':'Farsi'},{'name':'French','value':'French'},{'name':'Georgian','value':'Georgian'},{'name':'German','value':'German'},{'name':'German (Austria)','value':'German (Austria)'},{'name':'German (Switzerland)','value':'German (Switzerland)'},{'name':'Great Britain (English)','value':'Great Britain (English)'},{'name':'India (English)','value':'India (English)'},{'name':'Indonesia','value':'Indonesia'},{'name':'Ireland (English)','value':'Ireland (English)'},{'name':'Italian','value':'Italian'},{'name':'Japanese','value':'Japanese'},{'name':'Korean','value':'Korean'},{'name':'Nepalese','value':'Nepalese'},{'name':'Norwegian','value':'Norwegian'},{'name':'Polish','value':'Polish'},{'name':'Portuguese (Brazil)','value':'Portuguese (Brazil)'},{'name':'Russian','value':'Russian'},{'name':'Slovakian','value':'Slovakian'},{'name':'Spanish','value':'Spanish'},{'name':'Spanish Mexico','value':'Spanish Mexico'},{'name':'Swedish','value':'Swedish'},{'name':'Turkish','value':'Turkish'},{'name':'Ukrainian','value':'Ukrainian'},{'name':'United States (English)','value':'United States (English)'},{'name':'Vietnamese','value':'Vietnamese'}]}]},{'k':'getProfileByEmployeeData','d':'Get LinkedIn URI By First, Last And Company','ed':'Get LinkedIn employee profile URI by firstname, lastname and company without manual search on Google or LinkedIn.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'Miquel'},{'n':'lastname','r':true,'t':'string','p':'Colomer'},{'n':'company','r':true,'t':'string','p':'uProc'}]},{'k':'getSurnameByPrefix','d':'Get Surname By Prefix (ES)','ed':'Get first personal surname matching by prefix from INE data source (only Spain)','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Col'}]},{'k':'checkSurnameExist','d':'Check Surname Is Valid (ES)','ed':'Check if a personal surname appears in INE data source (only Spain)','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Gonzalez'}]},{'k':'getProfileLinkedinByEmail','d':'Get LinkedIn URI By Email','ed':'Get LinkedIn employee profile URI by business email.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getSurnameListByPrefix','d':'Get Surnames By Prefix (ES)','ed':'Get personal surnames matching by prefix from INE data source (only Spain)','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Co'}]},{'k':'getSurnameNormalized','d':'Get Normalized Surname','ed':'Normalize surname','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'C0lomer'}]},{'k':'getProfileTwitterByEmployeeData','d':'Get Twitter URI By First, Last And Company','ed':'Get Twitter profile by firstname, lastname and company without manual search on Google or Twitter.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'Carlos'},{'n':'lastname','r':true,'t':'string','p':'Blanco'},{'n':'company','r':true,'t':'string','p':'Encomenda'}]},{'k':'checkSurnameValid','d':'Check Surname Has Valid Format','ed':'Check if surname contains accepted characters','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Gonzalez'}]},{'k':'getProfileXingByEmployeeData','d':'Get Xing URI By First, Last And Company','ed':'Get Xing profile by firstname, lastname and company without manual search on Google or Xing.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'David'},{'n':'lastname','r':true,'t':'string','p':'Tomás'},{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getPhoneFixed','d':'Get Fixed Phone','ed':'Fix the international prefix of a phone based on the ISO code of a country','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'+1605281220'},{'n':'country','r':true,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'checkPhoneFormat','d':'Check Valid Phone Format By Country','ed':'Allow to discover if landline number has a good international format, depending on country ','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'},{'n':'country','r':true,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'checkPhoneFormatEs','d':'Check Valid Phone Format (ES)','ed':'Discover if landline phone number is valid, with Spain coverage','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'932187670'}]},{'k':'getPhoneNormalized','d':'Get Cleaned Phone','ed':'Clean a phone removing non allowed characters','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'3493218 767o'},{'n':'country','r':false,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'checkPhoneOrMobileValid','d':'Check Phone Or Mobile Valid','ed':'Discover if landline or mobile number has a valid prefix','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'},{'n':'country','r':false,'t':'string','p':'ES'}]},{'k':'getPhoneParsed','d':'Get Parsed And Validated Phone','ed':'Parse phone number in multiple fields and verify format and prefix validity (phone existence is not checked).','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'}]},{'k':'checkPhoneValidPrefix','d':'Check Valid Phone Prefix','ed':'Discover if a landline phone number prefix exists, with worldwide coverage','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'}]},{'k':'getProvinceByIp','d':'Get Province By IP','ed':'Discover the province name from an IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getProvinceByName','d':'Get Province By Name (ES)','ed':'You can get the first province by a name prefix (only Spain)','g':'geographic','p':[{'n':'province','r':true,'t':'string','p':'B'}]},{'k':'getProvinceByPhone','d':'Get Province By Phone (ES)','ed':'Discover the province name from a landline phone number (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'932'}]},{'k':'getProvinceByZipcode','d':'Get Province By Zipcode (ES)','ed':'Discover the province name from a zipcode number (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08'}]},{'k':'getProvinceListByName','d':'Get Provinces By Name (ES)','ed':'You can get a province list by a name prefix (only Spain)','g':'geographic','p':[{'n':'province','r':true,'t':'string','p':'B'}]},{'k':'getProvinceListByPhone','d':'Get Provinces By Phone (ES)','ed':'You can get a province list by a phone prefix (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'932'}]},{'k':'getProvinceListByZipcode','d':'Get Provinces By Zipcode (ES)','ed':'You can get a province list by a zipcode prefix, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'0'}]},{'k':'getProvinceNormalized','d':'Get Normalized Province','ed':'Allow to normalize a province, removing non allowed characters','g':'geographic','p':[{'n':'province','r':true,'t':'string','p':'Barce lona'}]},{'k':'checkRobinsonEmailExist','d':'Check Email Is Robinson (ES)','ed':'Discover if an email exists in the Robinson list (only Spain)','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'manzaned@uvigo.es'}]},{'k':'checkRobinsonNifExist','d':'Check Nif Number Is Robinson (ES)','ed':'Discover if an nif exists in the Robinson list (only Spain)','g':'personal','p':[{'n':'nif','r':true,'t':'string','p':'00002206K'}]},{'k':'checkRobinsonPhoneExist','d':'Check Phone Is Robinson (ES)','ed':'Discover if a phone (landline or mobile) exists in a Robinson list (only Spain)','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'917156252'}]},{'k':'getSentimentByText','d':'Get Sentiment From A Text','ed':'It allows to analyze an english text with Emojis and detect sentiment','g':'text','p':[{'n':'text','r':true,'t':'string','p':'I am very happy'}]},{'k':'checkSocialDomainExist','d':'Check Domain Has Social Activity','ed':'Discover if a domain has social network presence','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getSocialDomainLookup','d':'Get Social Networks By Domain','ed':'Discover if a domain or a website has social activity and returns all social network profiles found','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getSocialDomainParsed','d':'Get Social Networks Activity By Domain','ed':'Search all social networks by domain, parses all found urls and returns social networks kpis (if data available)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'checkSocialEmailExist','d':'Check Email Has Social Activity','ed':'Discover if the email has social network presence','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'mcolomer@gmail.com'}]},{'k':'getSocialEmailLookup','d':'Get Social Networks By Email','ed':'Discover if an email has social activity, and get all social network profiles found','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkSocialMobileExist','d':'Check Mobile Has Social Activity','ed':'Discover if the mobile phone has social network presence','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getSocialMobileLookup','d':'Get Social Networks By Mobile','ed':'Discover if an mobile phone has social activity, and get all social network profiles found','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getSocialUriParsed','d':'Get Social Network Activity','ed':'This tools parses a social uri address and extracts any available indicators','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.facebook.com/uprocdataquality'}]},{'k':'checkStringAlpha','d':'Check Alphabetic String Is Valid','ed':'Check if string length contains only letters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'asc'}]},{'k':'checkStringAlphanumeric','d':'Check Alphanumeric String Is','ed':'Check if string length contains only numbers and letters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'aa11'}]},{'k':'getStringBase64','d':'Get BASE64 Value','ed':'Convert a string to a BASE64 encoded value','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'}]},{'k':'checkStringBlank','d':'Check String Is Empty','ed':'Check if string has no content','g':'text','p':[{'n':'text','r':true,'t':'string','p':''}]},{'k':'checkStringBoolean','d':'Check Boolean String Is Valid','ed':'Check if string is true or false','g':'text','p':[{'n':'text','r':true,'t':'string','p':'true'}]},{'k':'getStringByFormat','d':'Get Formatted String By Pattern','ed':'It allows to format a string using a format pattern','g':'text','p':[{'n':'text','r':true,'t':'string','p':'1122'},{'n':'format','r':true,'t':'string','p':'%09d'}]},{'k':'getStringByRegex','d':'Get Generated Text By Pattern','ed':'Generate a random string using a regular expression as a pattern','g':'text','p':[{'n':'regex','r':true,'t':'string','p':'[0-9]{,2}-[a-zA-Z]{2,3}-[A-Z]{2,5}-[0-9]{2}-[a-z]*'}]},{'k':'checkStringContains','d':'Check String Contains Char','ed':'Check if string contains a character','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'test'},{'n':'text2','r':true,'t':'string','p':'t'}]},{'k':'checkStringEnds','d':'Check String Ends With','ed':'Check if string ends with a character','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'test'},{'n':'text2','r':true,'t':'string','p':'t'}]},{'k':'getStringFieldName','d':'Get Field Type By Value','ed':'Get field name, analyzing field value provided.

Supported values: Email, Domain, Isbn, Ean, Upc, Dni, Nie, Cif, Date, Gender (male, female), Landline, Mobile phone, Zip code, Web address','g':'text','p':[{'n':'text','r':true,'t':'string','p':'myemail@mydomain.com'}]},{'k':'getStringJoin','d':'Get Merged Values By Text','ed':'Join a character or string to join two values','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'hi'},{'n':'text2','r':true,'t':'string','p':'good morning!'},{'n':'glue','r':true,'t':'string','p':','}]},{'k':'getStringLength','d':'Get String Length','ed':'Get length of a string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, Mike!'}]},{'k':'checkStringLengthBetw','d':'Check String Length Is Between','ed':'Check if string length is between two numbers','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length1','r':true,'t':'number','p':'3'},{'n':'length2','r':true,'t':'number','p':'5'}]},{'k':'checkStringLengthEq','d':'Check String Length Is Equal','ed':'Check if string length is equal to number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkStringLengthGe','d':'Check String Length Is Greater Or Equal','ed':'Check if string length is greater or equal than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkStringLengthGt','d':'Check String Length Is Greater','ed':'Check if string length is greater than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkStringLengthLe','d':'Check String Length Is Lower Or Equal','ed':'Check if string length is lower or equal than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkStringLengthLt','d':'Check String Length Is Lower','ed':'Check if string length is lower than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'5'}]},{'k':'checkStringLowercase','d':'Check Text Is Lowercased','ed':'Check if string only contains lowercase characters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'aaa'}]},{'k':'getStringLowercase','d':'Get Lowercased Text','ed':'Convert all letters found in a string to lowercase','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, Mike!'}]},{'k':'getStringMd5','d':'Get MD5 String','ed':'Convert a string to a MD5 encoded value','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'}]},{'k':'getStringNormalized','d':'Get Normalized String By Field','ed':'Normalize a string depending on the field name','g':'text','p':[{'n':'field','r':true,'t':'options','p':'name','o':[{'name':'Alphabetic','value':'alphabetic'},{'name':'Alphanumeric','value':'alphanumeric'},{'name':'Cif','value':'cif'},{'name':'City','value':'city'},{'name':'Country','value':'country'},{'name':'Date','value':'date'},{'name':'Decimal','value':'decimal'},{'name':'Dni','value':'dni'},{'name':'Domain','value':'domain'},{'name':'Email','value':'email'},{'name':'Gender','value':'gender'},{'name':'Integer','value':'integer'},{'name':'Ip','value':'ip'},{'name':'Mobile','value':'mobile'},{'name':'Name','value':'name'},{'name':'Nie','value':'nie'},{'name':'Nif','value':'nif'},{'name':'Phone','value':'phone'},{'name':'Province','value':'province'},{'name':'Zipcode','value':'zipcode'}]},{'n':'text','r':true,'t':'string','p':'JMª Gº.Fco.gtez. Gro.'}]},{'k':'getStringParsed','d':'Get Parsed Text','ed':'Analyze string and return all emails, phones, zipcodes and links detected','g':'text','p':[{'n':'text','r':true,'t':'string','p':'My email is miquel@uproc.io and my phone is 34605281220'}]},{'k':'checkStringRandom','d':'Check String Is Random','ed':'Check if string contains random characters without sense','g':'text','p':[{'n':'text','r':true,'t':'string','p':'t2 chhsdfitoixcv'}]},{'k':'checkStringRegex','d':'Check String Is Valid By Pattern','ed':'Check if string contains a value that matches with a regular expression','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'regex','r':true,'t':'string','p':'^test$'}]},{'k':'getStringReplaceAll','d':'Get Texts Replaced By String','ed':'Replace all values found in a string by another','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, good morning!'},{'n':'find','r':true,'t':'string','p':'o'},{'n':'replace','r':true,'t':'string','p':'u'}]},{'k':'getStringReplaceFirst','d':'Get Text Replaced By String','ed':'Replace first value found in a string by another','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, good morning!'},{'n':'find','r':true,'t':'string','p':'o'},{'n':'replace','r':true,'t':'string','p':'u'}]},{'k':'getStringSha','d':'Get SHA String','ed':'Convert a string to a SHA encoded value','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'}]},{'k':'getStringSplit','d':'Get Splitted Values By Separator','ed':'Split a value in two parts, using a separator present in the original string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Texto largo, separado por coma'},{'n':'separator','r':true,'t':'string','p':','}]},{'k':'getStringSplitAndJoin','d':'Get Splitted And Merged Values By Separator','ed':'Split a value in two parts and join them, using a separator present in the original string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, good morning!'},{'n':'separator','r':true,'t':'string','p':','},{'n':'glue','r':true,'t':'string','p':';'}]},{'k':'checkStringStarts','d':'Check String Starts With','ed':'Check if string starts with a character','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'test'},{'n':'text2','r':true,'t':'string','p':'t'}]},{'k':'getStringTranslated','d':'Get Translated Text','ed':'It allows to translate a text to any language','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, my name is Mike'},{'n':'language','r':true,'t':'options','p':'Spanish','o':[{'name':'Afrikaans','value':'Afrikaans'},{'name':'Albanian','value':'Albanian'},{'name':'Amharic','value':'Amharic'},{'name':'Arabic','value':'Arabic'},{'name':'Armenian','value':'Armenian'},{'name':'Azeerbaijani','value':'Azeerbaijani'},{'name':'Basque','value':'Basque'},{'name':'Belarusian','value':'Belarusian'},{'name':'Bengali','value':'Bengali'},{'name':'Bosnian','value':'Bosnian'},{'name':'Bulgarian','value':'Bulgarian'},{'name':'Catalan','value':'Catalan'},{'name':'Cebuano','value':'Cebuano'},{'name':'Chinese (Simplified)','value':'Chinese (Simplified)'},{'name':'Chinese (Traditional)','value':'Chinese (Traditional)'},{'name':'Corsican','value':'Corsican'},{'name':'Croatian','value':'Croatian'},{'name':'Czech','value':'Czech'},{'name':'Danish','value':'Danish'},{'name':'Dutch','value':'Dutch'},{'name':'English','value':'English'},{'name':'Esperanto','value':'Esperanto'},{'name':'Estonian','value':'Estonian'},{'name':'Finnish','value':'Finnish'},{'name':'French','value':'French'},{'name':'Frisian','value':'Frisian'},{'name':'Galician','value':'Galician'},{'name':'Georgian','value':'Georgian'},{'name':'German','value':'German'},{'name':'Greek','value':'Greek'},{'name':'Gujarati','value':'Gujarati'},{'name':'Haitian Creole','value':'Haitian Creole'},{'name':'Hausa','value':'Hausa'},{'name':'Hawaiian','value':'Hawaiian'},{'name':'Hebrew','value':'Hebrew'},{'name':'Hindi','value':'Hindi'},{'name':'Hmong','value':'Hmong'},{'name':'Hungarian','value':'Hungarian'},{'name':'Icelandic','value':'Icelandic'},{'name':'Igbo','value':'Igbo'},{'name':'Indonesian','value':'Indonesian'},{'name':'Irish','value':'Irish'},{'name':'Italian','value':'Italian'},{'name':'Japanese','value':'Japanese'},{'name':'Javanese','value':'Javanese'},{'name':'Kannada','value':'Kannada'},{'name':'Kazakh','value':'Kazakh'},{'name':'Khmer','value':'Khmer'},{'name':'Korean','value':'Korean'},{'name':'Kurdish','value':'Kurdish'},{'name':'Kyrgyz','value':'Kyrgyz'},{'name':'Lao','value':'Lao'},{'name':'Latin','value':'Latin'},{'name':'Latvian','value':'Latvian'},{'name':'Lithuanian','value':'Lithuanian'},{'name':'Luxembourgish','value':'Luxembourgish'},{'name':'Macedonian','value':'Macedonian'},{'name':'Malagasy','value':'Malagasy'},{'name':'Malay','value':'Malay'},{'name':'Malayalam','value':'Malayalam'},{'name':'Maltese','value':'Maltese'},{'name':'Maori','value':'Maori'},{'name':'Marathi','value':'Marathi'},{'name':'Mongolian','value':'Mongolian'},{'name':'Myanmar (Burmese)','value':'Myanmar (Burmese)'},{'name':'Nepali','value':'Nepali'},{'name':'Norwegian','value':'Norwegian'},{'name':'Nyanja (Chichewa)','value':'Nyanja (Chichewa)'},{'name':'Pashto','value':'Pashto'},{'name':'Persian','value':'Persian'},{'name':'Polish','value':'Polish'},{'name':'Portuguese (Portugal, Brazil)','value':'Portuguese (Portugal, Brazil)'},{'name':'Punjabi','value':'Punjabi'},{'name':'Romanian','value':'Romanian'},{'name':'Russian','value':'Russian'},{'name':'Samoan','value':'Samoan'},{'name':'Scots Gaelic','value':'Scots Gaelic'},{'name':'Serbian','value':'Serbian'},{'name':'Sesotho','value':'Sesotho'},{'name':'Shona','value':'Shona'},{'name':'Sindhi','value':'Sindhi'},{'name':'Sinhala (Sinhalese)','value':'Sinhala (Sinhalese)'},{'name':'Slovak','value':'Slovak'},{'name':'Slovenian','value':'Slovenian'},{'name':'Somali','value':'Somali'},{'name':'Spanish','value':'Spanish'},{'name':'Sundanese','value':'Sundanese'},{'name':'Swahili','value':'Swahili'},{'name':'Swedish','value':'Swedish'},{'name':'Tagalog (Filipino)','value':'Tagalog (Filipino)'},{'name':'Tajik','value':'Tajik'},{'name':'Tamil','value':'Tamil'},{'name':'Telugu','value':'Telugu'},{'name':'Thai','value':'Thai'},{'name':'Turkish','value':'Turkish'},{'name':'Ukrainian','value':'Ukrainian'},{'name':'Urdu','value':'Urdu'},{'name':'Uzbek','value':'Uzbek'},{'name':'Vietnamese','value':'Vietnamese'},{'name':'Welsh','value':'Welsh'},{'name':'Xhosa','value':'Xhosa'},{'name':'Yiddish','value':'Yiddish'},{'name':'Yoruba','value':'Yoruba'},{'name':'Zulu','value':'Zulu'}]}]},{'k':'checkStringUppercase','d':'Check Text Is Uppercased','ed':'Check if string only contains uppercase characters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'AAA'}]},{'k':'getStringUppercase','d':'Get Uppercased Text','ed':'Convert all letters found in a string to uppercase','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, Mike!'}]},{'k':'getStringVlookup','d':'Get String VLookup','ed':'Lookup string between multiple values by fuzzy logic and regex patterns','g':'text','p':[{'n':'text','r':true,'t':'string','p':'s4mple'},{'n':'texts','r':true,'t':'string','p':'sample,samples'}]},{'k':'getVatByAddress','d':'Get VAT% By Address','ed':'Get country VAT by address','g':'finance','p':[{'n':'address','r':true,'t':'string','p':'Mallorca, 120 España'}]},{'k':'getVatByCoordinates','d':'Get VAT% By Coordinates','ed':'Get country VAT by coordinates','g':'finance','p':[{'n':'coordinates','r':true,'t':'string','p':'41.61921,2.2904413'}]},{'k':'getVatByIp','d':'Get VAT% By IP','ed':'Get VAT by IP address','g':'finance','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getVatByIsocode','d':'Get VAT% By ISO Code','ed':'Get VAT value by country ISO code','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getVatByNumber','d':'Get Data By TIN (VIES)','ed':'Get related european TIN number in Europe','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'ES'},{'n':'tin','r':true,'t':'string','p':'44016116G'}]},{'k':'getVatByPhone','d':'Get VAT% By Phone','ed':'Get VAT by phone number, with worldwide coverage','g':'finance','p':[{'n':'phone','r':true,'t':'string','p':'57122000111'}]},{'k':'getVatByZipcode','d':'Get VAT% By Zipcode','ed':'Get VAT by zipcode','g':'finance','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'checkVatExist','d':'Check TIN Exists (VIES)','ed':'Check if TIN number exists in Europe','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'ES'},{'n':'tin','r':true,'t':'string','p':'44016116G'}]},{'k':'getWordBanned','d':'Get Banned Words','ed':'Discover English banned words in the email body or subject','g':'text','p':[{'n':'text','r':true,'t':'string','p':'I am so thrilled to inform you that our new amazing feature is live!'}]},{'k':'getWordCleanAbuse','d':'Get Text Cleaned Without Abuse Words','ed':'Clean abuse words from a string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'comentario de un maldito personaje'}]},{'k':'getWordCount','d':'Get Words Count','ed':'Count total words in a text','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'}]},{'k':'checkWordCountBetw','d':'Check Word Count Between','ed':'Check if the number of words in a sentence is between two determined quantities','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count1','r':true,'t':'string','p':'1'},{'n':'count2','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountEq','d':'Check Word Count Equal','ed':'Check if the number of words in a sentence equals a certain amount','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountGe','d':'Check Word Count Greater Or Equal','ed':'Check if the number of words in a sentence is greater than or equal to a certain amount','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountGt','d':'Check Word Count Greater','ed':'Check if the number of words in a sentence is greater than a certain amount','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'1'}]},{'k':'checkWordCountLe','d':'Check Word Count Lower Or Equal','ed':'Check if the number of words present in a sentence is less than or equal to a quantity','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountLt','d':'Check Word Count Lower','ed':'','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'3'}]},{'k':'getZipcodeByIp','d':'Get Zipcode By IP','ed':'Discover the zipcode if you have an IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getZipcodeByPrefix','d':'Get Zipcode By Prefix (ES)','ed':'Get first zipcode by prefix, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'080'}]},{'k':'checkZipcodeExist','d':'Check Zipcode By Prefix Exists (ES)','ed':'Discover if a zipcode number prefix exists, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'checkZipcodeFormat','d':'Check Zipcode Has Valid Format (ES)','ed':'Discover if a zipcode number has a valid format, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getZipcodeListByPrefix','d':'Get Zipcodes By Prefix (ES)','ed':'Get multiple zipcodes by prefix, with worldwide coverage','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'080'}]},{'k':'getZipcodeNormalized','d':'Get Normalized Zipcode','ed':'Allow to normalize a zipcode, removing non allowed characters','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08i 12'}]}]}; \ No newline at end of file +export const tools = {processors: [{'k':'checkCreditcardChecksum','d':'Check Card Number Has Valid Format','ed':'Check if credit card number checksum is valid (Visa, Mastercard, Diners Club, Carte Blanche, American Express, Discover, JCB, enRoute, Solo, Switch, Maestro, LaserCard, ChinaUnionPay, BankCard, Voyager)','g':'finance','p':[{'n':'credit_card','r':true,'t':'string','p':'4024007151839544'}]},{'k':'getCreditcardType','d':'Get Credit Card Type By Number','ed':'Get credit card type (Visa, Mastercard, Diners Club, Carte Blanche, American Express, Discover, JCB, enRoute, Solo, Switch, Maestro, LaserCard, ChinaUnionPay, BankCard, Voyager)','g':'finance','p':[{'n':'credit_card','r':true,'t':'string','p':'4024007151839544'}]},{'k':'getAddressBySearch','d':'Get Exact Address By Search','ed':'Get an exact address (street name, number, city, zipcode, province, region, country, latitude and longitude) by a partial address search','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120, Barcelona, España'}]},{'k':'getCoordinateBySearch','d':'Get Coordinates By Search','ed':'Discover latitude and longitude coordinates of a postal address','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Urgell, 120, Barcelona, España'}]},{'k':'checkAddressExist','d':'Check Exact Address Exists','ed':'Check if an exact address exists by a partial address search','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Urgell, 120, Barcelona'},{'n':'country','r':false,'t':'string','p':'ES'}]},{'k':'getAddressNormalized','d':'Get Normalized Address','ed':'Allow to normalize an address, removing non allowed characters','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120, Barcelona Spain'}]},{'k':'checkAddressNumberExist','d':'Check Street Number Exists','ed':'Check if a house number exists by a partial address search','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Urgell, 120, Barcelona'},{'n':'country','r':false,'t':'string','p':'ES'}]},{'k':'getAddressSplitted','d':'Get Parsed Address','ed':'Parse postal address into separated fields, getting a basic resolution','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120 08036 Barcelona Barcelona Spain'}]},{'k':'getAddressSplittedBest','d':'Get Improved Parsed Address','ed':'Parse postal address into separated fields, getting an improved resolution','g':'geographic','p':[{'n':'address','r':true,'t':'string','p':'Compte Mallorca, 120 08036 Barcelona Barcelona Spain'}]},{'k':'checkCoordinateValid','d':'Check Valid Coordinates','ed':'Check if coordinates have a valid format','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.2522,-12.30'}]},{'k':'checkAgeBetw','d':'Check Age Between','ed':'Check if age is between two numbers','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years1','r':true,'t':'number','p':'35'},{'n':'years2','r':true,'t':'number','p':'50'}]},{'k':'getAgeByDate','d':'Get Age By Date','ed':'Discover the age of a birth date (multiple formats allowed)','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'checkAgeEq','d':'Check Ages Are Equal','ed':'Check if ages are equal','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'45'}]},{'k':'checkAgeGe','d':'Check Age Is Greater Or Equal','ed':'Check if age is greater or equal than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'43'}]},{'k':'checkAgeGt','d':'Check Age Is Greater','ed':'Check if age is greater than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'40'}]},{'k':'checkAgeIsAdult','d':'Check Age Is Greater Than Or Equal To 18 Years','ed':'Check if birth date belongs to an adult: 18 years old (Spain)','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1950-05-20'}]},{'k':'checkAgeIsForties','d':'Check Age Is Between 40 And 49 Years','ed':'Check if date returns an age between 40 and 49 years old','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'checkAgeIsRetired','d':'Check Age Is Greater Than 64 Years','ed':'Check if birth date returns an age greater than 64 years old','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1954-05-20'}]},{'k':'checkAgeIsTwenties','d':'Check Age Is Between 20 And 29 Years','ed':'Check if date returns an age between 20 and 29 years old','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'2000-05-20'}]},{'k':'checkAgeLe','d':'Check Age Is Lower Or Equal','ed':'Check if age is lower or equal than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'46'}]},{'k':'checkAgeLt','d':'Check Age Is Lower','ed':'Chekc if age is lower than another','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'},{'n':'years','r':true,'t':'number','p':'46'}]},{'k':'getAgeRange','d':'Get Age Range By Date','ed':'Discover the age range of a person by birth date','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'getAsinByEan','d':'Get ASIN By EAN','ed':'Get ASIN code by EAN code querying Internet.','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'0635753490879'}]},{'k':'checkAsinExist','d':'Check ASIN Exists','ed':'Check if a ASIN code exists on Amazon marketplace','g':'product','p':[{'n':'asin','r':true,'t':'string','p':'B00005N5PF'}]},{'k':'checkAsinValid','d':'Check ASIN Valid','ed':'Check if a ASIN code has a valid format','g':'product','p':[{'n':'asin','r':true,'t':'string','p':'B00005N5PF'}]},{'k':'getAudioAdvancedSpeechByText','d':'Get Advanced Speech By Text','ed':'Get advanced human audio file by provided text and language','g':'audio','p':[{'n':'text','r':true,'t':'string','p':'Hi! My name is Miquel. I will read any text you type here.'},{'n':'gender','r':true,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]},{'n':'language','r':true,'t':'options','p':'american','o':[{'name':'American','value':'american'},{'name':'Arabic','value':'arabic'},{'name':'Bengali','value':'bengali'},{'name':'British','value':'british'},{'name':'Czech','value':'czech'},{'name':'Danish','value':'danish'},{'name':'Dutch','value':'dutch'},{'name':'Filipino','value':'filipino'},{'name':'Finnish','value':'finnish'},{'name':'French','value':'french'},{'name':'German','value':'german'},{'name':'Greek','value':'greek'},{'name':'Gujurati','value':'gujurati'},{'name':'Hindi','value':'hindi'},{'name':'Hungarian','value':'hungarian'},{'name':'Indonesian','value':'indonesian'},{'name':'Italian','value':'italian'},{'name':'Japanese','value':'japanese'},{'name':'Kannada','value':'kannada'},{'name':'Korean','value':'korean'},{'name':'Malayalam','value':'malayalam'},{'name':'Mandarin','value':'mandarin'},{'name':'Norwegian','value':'norwegian'},{'name':'Polish','value':'polish'},{'name':'Portuguese','value':'portuguese'},{'name':'Russian','value':'russian'},{'name':'Slovak','value':'slovak'},{'name':'Spanish','value':'spanish'},{'name':'Tamil','value':'tamil'},{'name':'Telugu','value':'telugu'},{'name':'Thai','value':'thai'},{'name':'Turkish','value':'turkish'},{'name':'Ukranian','value':'ukranian'},{'name':'Vietnamese','value':'vietnamese'}]}]},{'k':'getAudioSpeechByText','d':'Get Speech By Text','ed':'Get audio file by provided text and language','g':'audio','p':[{'n':'text','r':true,'t':'string','p':'Hi! My name is Miquel. I will read any text you type here.'},{'n':'gender','r':true,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]},{'n':'language','r':true,'t':'options','p':'american','o':[{'name':'American','value':'american'},{'name':'French','value':'french'},{'name':'German','value':'german'},{'name':'Italian','value':'italian'},{'name':'Japanese','value':'japanese'},{'name':'Portuguese','value':'portuguese'},{'name':'Russian','value':'russian'},{'name':'Spanish','value':'spanish'}]}]},{'k':'checkBankAccountValidEs','d':'Check Bank Account Is Valid (ES)','ed':'Discover if account number has a valid format','g':'finance','p':[{'n':'account','r':true,'t':'string','p':'14650120311716144388'}]},{'k':'checkBankBicValid','d':'Check BIC Is Valid','ed':'Discover if BIC number has a valid format','g':'finance','p':[{'n':'bic','r':true,'t':'string','p':'DABAIE2D'}]},{'k':'getBankIbanByAccount','d':'Get IBAN By Account','ed':'Get IBAN number by account number of the country','g':'finance','p':[{'n':'account','r':true,'t':'string','p':'14650120311716144388'},{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getBankIbanLookup','d':'Get IBAN Lookup','ed':'Get to search data bank information by IBAN account number','g':'finance','p':[{'n':'iban','r':true,'t':'string','p':'NL91ABNA0417164300'}]},{'k':'checkBankIbanValid','d':'Check IBAN Is Valid','ed':'Discover if IBAN account number has a valid format','g':'finance','p':[{'n':'iban','r':true,'t':'string','p':'ES3314650120311716144388'}]},{'k':'getBarcodeEncoded','d':'Get Encoded Barcode','ed':'Get an encoded barcode by number and required standard','g':'image','p':[{'n':'text','r':true,'t':'string','p':'0635753490879'},{'n':'bcid','r':true,'t':'options','p':'ean13','o':[{'name':'Auspost','value':'auspost'},{'name':'Azteccode','value':'azteccode'},{'name':'Azteccodecompact','value':'azteccodecompact'},{'name':'Aztecrune','value':'aztecrune'},{'name':'Bc412','value':'bc412'},{'name':'Channelcode','value':'channelcode'},{'name':'Codablockf','value':'codablockf'},{'name':'Code11','value':'code11'},{'name':'Code128','value':'code128'},{'name':'Code16k','value':'code16k'},{'name':'Code2of5','value':'code2of5'},{'name':'Code32','value':'code32'},{'name':'Code39','value':'code39'},{'name':'Code39ext','value':'code39ext'},{'name':'Code49','value':'code49'},{'name':'Code93','value':'code93'},{'name':'Code93ext','value':'code93ext'},{'name':'Codeone','value':'codeone'},{'name':'Coop2of5','value':'coop2of5'},{'name':'Daft','value':'daft'},{'name':'Databarexpanded','value':'databarexpanded'},{'name':'Databarexpandedcomposite','value':'databarexpandedcomposite'},{'name':'Databarexpandedstacked','value':'databarexpandedstacked'},{'name':'Databarexpandedstackedcomposite','value':'databarexpandedstackedcomposite'},{'name':'Databarlimited','value':'databarlimited'},{'name':'Databarlimitedcomposite','value':'databarlimitedcomposite'},{'name':'Databaromni','value':'databaromni'},{'name':'Databaromnicomposite','value':'databaromnicomposite'},{'name':'Databarstacked','value':'databarstacked'},{'name':'Databarstackedcomposite','value':'databarstackedcomposite'},{'name':'Databarstackedomni','value':'databarstackedomni'},{'name':'Databarstackedomnicomposite','value':'databarstackedomnicomposite'},{'name':'Databartruncated','value':'databartruncated'},{'name':'Databartruncatedcomposite','value':'databartruncatedcomposite'},{'name':'Datalogic2of5','value':'datalogic2of5'},{'name':'Datamatrix','value':'datamatrix'},{'name':'Datamatrixrectangular','value':'datamatrixrectangular'},{'name':'Dotcode','value':'dotcode'},{'name':'Ean13','value':'ean13'},{'name':'Ean13composite','value':'ean13composite'},{'name':'Ean14','value':'ean14'},{'name':'Ean2','value':'ean2'},{'name':'Ean5','value':'ean5'},{'name':'Ean8','value':'ean8'},{'name':'Ean8composite','value':'ean8composite'},{'name':'Flattermarken','value':'flattermarken'},{'name':'Gs1-128','value':'gs1-128'},{'name':'Gs1-128composite','value':'gs1-128composite'},{'name':'Gs1-cc','value':'gs1-cc'},{'name':'Gs1datamatrix','value':'gs1datamatrix'},{'name':'Gs1datamatrixrectangular','value':'gs1datamatrixrectangular'},{'name':'Gs1northamericancoupon','value':'gs1northamericancoupon'},{'name':'Hanxin','value':'hanxin'},{'name':'Hibcazteccode','value':'hibcazteccode'},{'name':'Hibccodablockf','value':'hibccodablockf'},{'name':'Hibccode128','value':'hibccode128'},{'name':'Hibccode39','value':'hibccode39'},{'name':'Hibcdatamatrix','value':'hibcdatamatrix'},{'name':'Hibcdatamatrixrectangular','value':'hibcdatamatrixrectangular'},{'name':'Hibcmicropdf417','value':'hibcmicropdf417'},{'name':'Hibcpdf417','value':'hibcpdf417'},{'name':'Iata2of5','value':'iata2of5'},{'name':'Identcode','value':'identcode'},{'name':'Industrial2of5','value':'industrial2of5'},{'name':'Interleaved2of5','value':'interleaved2of5'},{'name':'Isbn','value':'isbn'},{'name':'Ismn','value':'ismn'},{'name':'Issn','value':'issn'},{'name':'Itf14','value':'itf14'},{'name':'Japanpost','value':'japanpost'},{'name':'Kix','value':'kix'},{'name':'Leitcode','value':'leitcode'},{'name':'Matrix2of5','value':'matrix2of5'},{'name':'Maxicode','value':'maxicode'},{'name':'Micropdf417','value':'micropdf417'},{'name':'Msi','value':'msi'},{'name':'Onecode','value':'onecode'},{'name':'Pdf417','value':'pdf417'},{'name':'Pdf417compact','value':'pdf417compact'},{'name':'Pharmacode','value':'pharmacode'},{'name':'Pharmacode2','value':'pharmacode2'},{'name':'Planet','value':'planet'},{'name':'Plessey','value':'plessey'},{'name':'Posicode','value':'posicode'},{'name':'Postnet','value':'postnet'},{'name':'Pzn','value':'pzn'},{'name':'RationalizedCodabar','value':'rationalizedCodabar'},{'name':'Raw','value':'raw'},{'name':'Royalmail','value':'royalmail'},{'name':'Sscc18','value':'sscc18'},{'name':'Symbol','value':'symbol'},{'name':'Telepen','value':'telepen'},{'name':'Telepennumeric','value':'telepennumeric'},{'name':'Ultracode','value':'ultracode'},{'name':'Upca','value':'upca'},{'name':'Upcacomposite','value':'upcacomposite'},{'name':'Upce','value':'upce'},{'name':'Upcecomposite','value':'upcecomposite'}]}]},{'k':'getBookAuthorLookup','d':'Get Book By Author','ed':'Get book by author\'s surname','g':'product','p':[{'n':'author','r':true,'t':'string','p':'Albert Einstein'}]},{'k':'getBookCategoryLookup','d':'Get Book By Category','ed':'Get all publications by category','g':'product','p':[{'n':'category','r':true,'t':'string','p':'science'}]},{'k':'checkBookIsbn','d':'Check ISBN Code Is Valid','ed':'Allow to check if an ISBN10/13 code has a valid format','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'0306406152'}]},{'k':'checkBookIsbnExist','d':'Check ISBN Code Exists','ed':'Allow to check if an ISBN book exist','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'9780306406157'}]},{'k':'getBookIsbnLookup','d':'Get Book By ISBN','ed':'Get book or publication data by 10 or 13 digits ISBN code','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'9780306406157'}]},{'k':'checkBookIsbn10','d':'Check ISBN10 Code Is Valid','ed':'Allow to check if an ISBN10 code has a valid format','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'0306406152'}]},{'k':'checkBookIsbn13','d':'Check ISBN13 Code Is Valid','ed':'Allow to check if an ISBN13 code has a valid format','g':'product','p':[{'n':'isbn','r':true,'t':'string','p':'9780306406157'}]},{'k':'getBookListAuthorLookup','d':'Get Books By Author','ed':'Get books by author\'s surname','g':'product','p':[{'n':'author','r':true,'t':'string','p':'Albert Einstein'}]},{'k':'getBookListCategoryLookup','d':'Get Books By Category','ed':'Get all books by category','g':'product','p':[{'n':'category','r':true,'t':'string','p':'science'}]},{'k':'getBookListPublisherLookup','d':'Get Books By Editor','ed':'Get all books by editor','g':'product','p':[{'n':'publisher','r':true,'t':'string','p':'Grupo RBA'}]},{'k':'getBookListTitleLookup','d':'Get Books By Title','ed':'Get all books by title','g':'product','p':[{'n':'title','r':true,'t':'string','p':'Science'}]},{'k':'getBookPublisherLookup','d':'Get Book By Editor','ed':'Get book data by editor\'s name','g':'product','p':[{'n':'publisher','r':true,'t':'string','p':'Grupo RBA'}]},{'k':'getBookTitleLookup','d':'Get Book By Title','ed':'Get book data by title','g':'product','p':[{'n':'title','r':true,'t':'string','p':'La empresa más feliz del mundo'}]},{'k':'getNifByDni','d':'Get NIF By DNI (ES)','ed':'Discover the letter of a dni card number','g':'personal','p':[{'n':'dni','r':true,'t':'string','p':'44016116'}]},{'k':'getCifNormalized','d':'Get Normalized CIF','ed':'Allow to normalize a CIF number, removing non allowed characters','g':'company','p':[{'n':'cif','r':true,'t':'string','p':'B 62084 959'}]},{'k':'getDniNormalized','d':'Get Normalized DNI (ES)','ed':'Allow to normalize a DNI number, removing non allowed characters','g':'personal','p':[{'n':'dni','r':true,'t':'string','p':'44016a116'}]},{'k':'getNieNormalized','d':'Get Normalized NIE (ES)','ed':'Allow to normalize a NIE number, removing non allowed characters','g':'personal','p':[{'n':'nie','r':true,'t':'string','p':'X402001 122g'}]},{'k':'getNifNormalized','d':'Get Normalized NIF (ES)','ed':'Allow to normalize a NIF number, removing non allowed characters','g':'personal','p':[{'n':'nif','r':true,'t':'string','p':'402001 122g'}]},{'k':'checkCifValid','d':'Check CIF Is Valid (ES)','ed':'Discover if a cif card number is valid','g':'company','p':[{'n':'cif','r':true,'t':'string','p':'A58818501'}]},{'k':'checkDniValid','d':'Check Valid Dni (ES)','ed':'Discover if a dni card number is valid','g':'personal','p':[{'n':'dni','r':true,'t':'string','p':'44016116'}]},{'k':'checkNieValid','d':'Check NIE Is Valid (ES)','ed':'Discover if a NIE card number is valid','g':'personal','p':[{'n':'nie','r':true,'t':'string','p':'Y2918527W'}]},{'k':'checkNifValid','d':'Check NIF Is Valid (ES)','ed':'Discover if a nif card number is valid','g':'personal','p':[{'n':'nif','r':true,'t':'string','p':'44016116G'}]},{'k':'getCityByIp','d':'Get City By IP','ed':'Get city from ip','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCityByName','d':'Get City By Name (ES)','ed':'City search by partial name (only Spain)','g':'geographic','p':[{'n':'city','r':true,'t':'string','p':'Bar'}]},{'k':'getCityByPhone','d':'Get City By Phone (ES)','ed':'Discover the city name by the local phone number (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'932187670'}]},{'k':'getCityByZipcode','d':'Get City By Zipcode (ES)','ed':'Discover the city name by the zipcode (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getCityListByName','d':'Get Cities By Prefix (ES)','ed':'Get multiple cities by partial initial text (only Spain)','g':'geographic','p':[{'n':'city','r':true,'t':'string','p':'Barce'}]},{'k':'getCityListByPhone','d':'Get Cities By Phone (ES)','ed':'Get multiple cities by phone prefix (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'938499145'}]},{'k':'getCityListByZipcode','d':'Get Cities By Zipcode (ES)','ed':'Get multiple cities by zipcode prefix (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'234'}]},{'k':'getCityNormalized','d':'Get Normalized City','ed':'Allow to normalize a city, removing non allowed characters','g':'geographic','p':[{'n':'city','r':true,'t':'string','p':'Barc3l0na'}]},{'k':'checkEan13Valid','d':'Check EAN13 Is Valid','ed':'Check if a EAN barcode of 13 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'4006381333931'}]},{'k':'checkGtin13Valid','d':'Check GTIN13 Is Valid','ed':'Check if a GTIN barcode of 13 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'4006381333931'}]},{'k':'checkEan14Valid','d':'Check EAN14 Is Valid','ed':'Check if a EAN barcode of 14 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'04006381333931'}]},{'k':'checkGtin14Valid','d':'Check GTIN14 Is Valid','ed':'Check if a GTIN barcode of 14 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'04006381333931'}]},{'k':'checkEan18Valid','d':'Check EAN18 Is Valid','ed':'Check if a EAN barcode of 18 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'000004006381333931'}]},{'k':'checkEan8Valid','d':'Check EAN8 Is Valid','ed':'Check if a EAN barcode of 8 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'checkGtin8Valid','d':'Check GTIN8 Is Valid','ed':'Check if a GTIN barcode of 8 digits has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'getEanByAsin','d':'Get EAN By ASIN','ed':'Get EAN code by ASIN code querying Internet.','g':'product','p':[{'n':'asin','r':true,'t':'string','p':'B00005N5PF'}]},{'k':'checkEanExist','d':'Check EAN Exists','ed':'Check if a EAN code exists on Amazon Marketplace (.com supported)','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'0635753490879'}]},{'k':'checkUpcExist','d':'Check UPC Exists','ed':'Check if a UPC code exists','g':'product','p':[{'n':'upc','r':true,'t':'string','p':'635753490879'}]},{'k':'checkUpcFormat','d':'Check UPC Has Valid Format','ed':'Check if a UPC code has a valid format','g':'product','p':[{'n':'upc','r':true,'t':'string','p':'635753490879'}]},{'k':'checkNumberIsin','d':'Check ISIN Code Is Valid','ed':'Check if ISIN number is valid','g':'company','p':[{'n':'isin','r':true,'t':'string','p':'US0378331005'}]},{'k':'getUpcLookup','d':'Get Product By UPC','ed':'Get product data of an UPC code on Amazon Marketplace (.com supported)','g':'product','p':[{'n':'upc','r':true,'t':'string','p':'635753490879'}]},{'k':'checkNumberSsEs','d':'Check Social Security Number Is Valid (ES)','ed':'Check if SS number is valid, only for Spain','g':'company','p':[{'n':'number','r':true,'t':'string','p':'998239812282'}]},{'k':'checkNumberUuid','d':'Check UUID Number Is Valid','ed':'Check if it a valid UUID number','g':'security','p':[{'n':'uuid','r':true,'t':'string','p':'550e8400-e29b-41d4-a716-446655440000'}]},{'k':'checkEanValid','d':'Check EAN Is Valid','ed':'Check if a EAN barcode (8 or 13 digits) has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'checkGtinValid','d':'Check GTIN Is Valid','ed':'Check if a GTIN barcode (8 or 13 digits) has a valid format','g':'product','p':[{'n':'ean','r':true,'t':'string','p':'40063812'}]},{'k':'getCommunityByZipcode','d':'Get Community By Zipcode (ES)','ed':'Discover the community name from a zipcode number (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getCompanyByCif','d':'Get Company By CIF (ES)','ed':'Get company data by CIF','g':'company','p':[{'n':'cif','r':true,'t':'string','p':'B66998592'}]},{'k':'getCompanyByDomain','d':'Get Company By Domain','ed':'Get company data by domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'}]},{'k':'getCompanyByDuns','d':'Get Company By DUNS (ES)','ed':'Get company data by DUNS','g':'company','p':[{'n':'duns','r':true,'t':'string','p':'464016690'}]},{'k':'getCompanyByEmail','d':'Get Company By Email','ed':'Get company data by email','g':'company','p':[{'n':'email','r':true,'t':'string','p':'hello@killia.com'}]},{'k':'getCompanyByIp','d':'Get Company By IP','ed':'Get company data by IP address','g':'company','p':[{'n':'ip','r':true,'t':'string','p':'74.125.228.72'}]},{'k':'getCompanyByName','d':'Get Company By Name','ed':'Get company data by name','g':'company','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick S.L.'},{'n':'country','r':false,'t':'options','p':'Spain','o':[{'name':'Afghanistan','value':'Afghanistan'},{'name':'Albania','value':'Albania'},{'name':'Algeria','value':'Algeria'},{'name':'American Samoa','value':'American Samoa'},{'name':'Andorra','value':'Andorra'},{'name':'Angola','value':'Angola'},{'name':'Anguilla','value':'Anguilla'},{'name':'Antarctica','value':'Antarctica'},{'name':'Antigua And Barbuda','value':'Antigua And Barbuda'},{'name':'Argentina','value':'Argentina'},{'name':'Armenia','value':'Armenia'},{'name':'Aruba','value':'Aruba'},{'name':'Australia','value':'Australia'},{'name':'Austria','value':'Austria'},{'name':'Azerbaijan','value':'Azerbaijan'},{'name':'Bahamas','value':'Bahamas'},{'name':'Bahrain','value':'Bahrain'},{'name':'Bangladesh','value':'Bangladesh'},{'name':'Barbados','value':'Barbados'},{'name':'Belarus','value':'Belarus'},{'name':'Belgium','value':'Belgium'},{'name':'Belize','value':'Belize'},{'name':'Benin','value':'Benin'},{'name':'Bermuda','value':'Bermuda'},{'name':'Bhutan','value':'Bhutan'},{'name':'Bolivia','value':'Bolivia'},{'name':'Bosnia And Herzegovina','value':'Bosnia And Herzegovina'},{'name':'Botswana','value':'Botswana'},{'name':'Bouvet Island','value':'Bouvet Island'},{'name':'Brazil','value':'Brazil'},{'name':'British Indian Ocean Territory','value':'British Indian Ocean Territory'},{'name':'Brunei Darussalam','value':'Brunei Darussalam'},{'name':'Bulgaria','value':'Bulgaria'},{'name':'Burkina Faso','value':'Burkina Faso'},{'name':'Burundi','value':'Burundi'},{'name':'Cambodia','value':'Cambodia'},{'name':'Cameroon','value':'Cameroon'},{'name':'Canada','value':'Canada'},{'name':'Cape Verde','value':'Cape Verde'},{'name':'Cayman Islands','value':'Cayman Islands'},{'name':'Central African Republic','value':'Central African Republic'},{'name':'Chad','value':'Chad'},{'name':'Chile','value':'Chile'},{'name':'China','value':'China'},{'name':'Christmas Island','value':'Christmas Island'},{'name':'Cocos (keeling) Islands','value':'Cocos (keeling) Islands'},{'name':'Colombia','value':'Colombia'},{'name':'Comoros','value':'Comoros'},{'name':'Congo','value':'Congo'},{'name':'Congo, The Democratic Republic Of The','value':'Congo, The Democratic Republic Of The'},{'name':'Cook Islands','value':'Cook Islands'},{'name':'Costa Rica','value':'Costa Rica'},{'name':'Cote D\'ivoire','value':'Cote D\'ivoire'},{'name':'Croatia','value':'Croatia'},{'name':'Cuba','value':'Cuba'},{'name':'Cyprus','value':'Cyprus'},{'name':'Czech Republic','value':'Czech Republic'},{'name':'Denmark','value':'Denmark'},{'name':'Djibouti','value':'Djibouti'},{'name':'Dominica','value':'Dominica'},{'name':'Dominican Republic','value':'Dominican Republic'},{'name':'East Timor','value':'East Timor'},{'name':'Ecuador','value':'Ecuador'},{'name':'Egypt','value':'Egypt'},{'name':'El Salvador','value':'El Salvador'},{'name':'Equatorial Guinea','value':'Equatorial Guinea'},{'name':'Eritrea','value':'Eritrea'},{'name':'Estonia','value':'Estonia'},{'name':'Ethiopia','value':'Ethiopia'},{'name':'Falkland Islands (malvinas)','value':'Falkland Islands (malvinas)'},{'name':'Faroe Islands','value':'Faroe Islands'},{'name':'Fiji','value':'Fiji'},{'name':'Finland','value':'Finland'},{'name':'France','value':'France'},{'name':'French Guiana','value':'French Guiana'},{'name':'French Polynesia','value':'French Polynesia'},{'name':'French Southern Territories','value':'French Southern Territories'},{'name':'Gabon','value':'Gabon'},{'name':'Gambia','value':'Gambia'},{'name':'Georgia','value':'Georgia'},{'name':'Germany','value':'Germany'},{'name':'Ghana','value':'Ghana'},{'name':'Gibraltar','value':'Gibraltar'},{'name':'Greece','value':'Greece'},{'name':'Greenland','value':'Greenland'},{'name':'Grenada','value':'Grenada'},{'name':'Guadeloupe','value':'Guadeloupe'},{'name':'Guam','value':'Guam'},{'name':'Guatemala','value':'Guatemala'},{'name':'Guinea','value':'Guinea'},{'name':'Guinea-bissau','value':'Guinea-bissau'},{'name':'Guyana','value':'Guyana'},{'name':'Haiti','value':'Haiti'},{'name':'Heard Island And Mcdonald Islands','value':'Heard Island And Mcdonald Islands'},{'name':'Holy See (vatican City State)','value':'Holy See (vatican City State)'},{'name':'Honduras','value':'Honduras'},{'name':'Hong Kong','value':'Hong Kong'},{'name':'Hungary','value':'Hungary'},{'name':'Iceland','value':'Iceland'},{'name':'India','value':'India'},{'name':'Indonesia','value':'Indonesia'},{'name':'Iran, Islamic Republic Of','value':'Iran, Islamic Republic Of'},{'name':'Iraq','value':'Iraq'},{'name':'Ireland','value':'Ireland'},{'name':'Israel','value':'Israel'},{'name':'Italy','value':'Italy'},{'name':'Jamaica','value':'Jamaica'},{'name':'Japan','value':'Japan'},{'name':'Jordan','value':'Jordan'},{'name':'Kazakstan','value':'Kazakstan'},{'name':'Kenya','value':'Kenya'},{'name':'Kiribati','value':'Kiribati'},{'name':'Korea, Democratic People\'s Republic Of','value':'Korea, Democratic People\'s Republic Of'},{'name':'Korea, Republic Of','value':'Korea, Republic Of'},{'name':'Kosovo','value':'Kosovo'},{'name':'Kuwait','value':'Kuwait'},{'name':'Kyrgyzstan','value':'Kyrgyzstan'},{'name':'Lao People\'s Democratic Republic','value':'Lao People\'s Democratic Republic'},{'name':'Latvia','value':'Latvia'},{'name':'Lebanon','value':'Lebanon'},{'name':'Lesotho','value':'Lesotho'},{'name':'Liberia','value':'Liberia'},{'name':'Libyan Arab Jamahiriya','value':'Libyan Arab Jamahiriya'},{'name':'Liechtenstein','value':'Liechtenstein'},{'name':'Lithuania','value':'Lithuania'},{'name':'Luxembourg','value':'Luxembourg'},{'name':'Macau','value':'Macau'},{'name':'Macedonia, The Former Yugoslav Republic Of','value':'Macedonia, The Former Yugoslav Republic Of'},{'name':'Madagascar','value':'Madagascar'},{'name':'Malawi','value':'Malawi'},{'name':'Malaysia','value':'Malaysia'},{'name':'Maldives','value':'Maldives'},{'name':'Mali','value':'Mali'},{'name':'Malta','value':'Malta'},{'name':'Marshall Islands','value':'Marshall Islands'},{'name':'Martinique','value':'Martinique'},{'name':'Mauritania','value':'Mauritania'},{'name':'Mauritius','value':'Mauritius'},{'name':'Mayotte','value':'Mayotte'},{'name':'Mexico','value':'Mexico'},{'name':'Micronesia, Federated States Of','value':'Micronesia, Federated States Of'},{'name':'Moldova, Republic Of','value':'Moldova, Republic Of'},{'name':'Monaco','value':'Monaco'},{'name':'Mongolia','value':'Mongolia'},{'name':'Montenegro','value':'Montenegro'},{'name':'Montserrat','value':'Montserrat'},{'name':'Morocco','value':'Morocco'},{'name':'Mozambique','value':'Mozambique'},{'name':'Myanmar','value':'Myanmar'},{'name':'Namibia','value':'Namibia'},{'name':'Nauru','value':'Nauru'},{'name':'Nepal','value':'Nepal'},{'name':'Netherlands','value':'Netherlands'},{'name':'Netherlands Antilles','value':'Netherlands Antilles'},{'name':'New Caledonia','value':'New Caledonia'},{'name':'New Zealand','value':'New Zealand'},{'name':'Nicaragua','value':'Nicaragua'},{'name':'Niger','value':'Niger'},{'name':'Nigeria','value':'Nigeria'},{'name':'Niue','value':'Niue'},{'name':'Norfolk Island','value':'Norfolk Island'},{'name':'Northern Mariana Islands','value':'Northern Mariana Islands'},{'name':'Norway','value':'Norway'},{'name':'Oman','value':'Oman'},{'name':'Pakistan','value':'Pakistan'},{'name':'Palau','value':'Palau'},{'name':'Palestinian Territory, Occupied','value':'Palestinian Territory, Occupied'},{'name':'Panama','value':'Panama'},{'name':'Papua New Guinea','value':'Papua New Guinea'},{'name':'Paraguay','value':'Paraguay'},{'name':'Peru','value':'Peru'},{'name':'Philippines','value':'Philippines'},{'name':'Pitcairn','value':'Pitcairn'},{'name':'Poland','value':'Poland'},{'name':'Portugal','value':'Portugal'},{'name':'Puerto Rico','value':'Puerto Rico'},{'name':'Qatar','value':'Qatar'},{'name':'Reunion','value':'Reunion'},{'name':'Romania','value':'Romania'},{'name':'Russian Federation','value':'Russian Federation'},{'name':'Rwanda','value':'Rwanda'},{'name':'Saint Helena','value':'Saint Helena'},{'name':'Saint Kitts And Nevis','value':'Saint Kitts And Nevis'},{'name':'Saint Lucia','value':'Saint Lucia'},{'name':'Saint Pierre And Miquelon','value':'Saint Pierre And Miquelon'},{'name':'Saint Vincent And The Grenadines','value':'Saint Vincent And The Grenadines'},{'name':'Samoa','value':'Samoa'},{'name':'San Marino','value':'San Marino'},{'name':'Sao Tome And Principe','value':'Sao Tome And Principe'},{'name':'Saudi Arabia','value':'Saudi Arabia'},{'name':'Senegal','value':'Senegal'},{'name':'Serbia','value':'Serbia'},{'name':'Seychelles','value':'Seychelles'},{'name':'Sierra Leone','value':'Sierra Leone'},{'name':'Singapore','value':'Singapore'},{'name':'Slovakia','value':'Slovakia'},{'name':'Slovenia','value':'Slovenia'},{'name':'Solomon Islands','value':'Solomon Islands'},{'name':'Somalia','value':'Somalia'},{'name':'South Africa','value':'South Africa'},{'name':'South Georgia And The South Sandwich Islands','value':'South Georgia And The South Sandwich Islands'},{'name':'Spain','value':'Spain'},{'name':'Sri Lanka','value':'Sri Lanka'},{'name':'Sudan','value':'Sudan'},{'name':'Suriname','value':'Suriname'},{'name':'Svalbard And Jan Mayen','value':'Svalbard And Jan Mayen'},{'name':'Swaziland','value':'Swaziland'},{'name':'Sweden','value':'Sweden'},{'name':'Switzerland','value':'Switzerland'},{'name':'Syrian Arab Republic','value':'Syrian Arab Republic'},{'name':'Taiwan, Province Of China','value':'Taiwan, Province Of China'},{'name':'Tajikistan','value':'Tajikistan'},{'name':'Tanzania, United Republic Of','value':'Tanzania, United Republic Of'},{'name':'Thailand','value':'Thailand'},{'name':'Togo','value':'Togo'},{'name':'Tokelau','value':'Tokelau'},{'name':'Tonga','value':'Tonga'},{'name':'Trinidad And Tobago','value':'Trinidad And Tobago'},{'name':'Tunisia','value':'Tunisia'},{'name':'Turkey','value':'Turkey'},{'name':'Turkmenistan','value':'Turkmenistan'},{'name':'Turks And Caicos Islands','value':'Turks And Caicos Islands'},{'name':'Tuvalu','value':'Tuvalu'},{'name':'Uganda','value':'Uganda'},{'name':'Ukraine','value':'Ukraine'},{'name':'United Arab Emirates','value':'United Arab Emirates'},{'name':'United Kingdom','value':'United Kingdom'},{'name':'United States','value':'United States'},{'name':'United States Minor Outlying Islands','value':'United States Minor Outlying Islands'},{'name':'Uruguay','value':'Uruguay'},{'name':'Uzbekistan','value':'Uzbekistan'},{'name':'Vanuatu','value':'Vanuatu'},{'name':'Venezuela','value':'Venezuela'},{'name':'Viet Nam','value':'Viet Nam'},{'name':'Virgin Islands, British','value':'Virgin Islands, British'},{'name':'Virgin Islands, U.s.','value':'Virgin Islands, U.s.'},{'name':'Wallis And Futuna','value':'Wallis And Futuna'},{'name':'Western Sahara','value':'Western Sahara'},{'name':'Yemen','value':'Yemen'},{'name':'Zambia','value':'Zambia'},{'name':'Zimbabwe','value':'Zimbabwe'}]}]},{'k':'getCompanyByPhone','d':'Get Company By Phone','ed':'Get company data by phone number','g':'company','p':[{'n':'phone','r':true,'t':'string','p':'34933197570'}]},{'k':'getCompanyByProfile','d':'Get Company By Social Profile','ed':'Get company data by social network uri (LinkedIn, Twitter, ...)','g':'company','p':[{'n':'url','r':true,'t':'string','p':'https://twitter.com/Cloudflare'}]},{'k':'getPersonByProfile','d':'Get Person By Social Profile','ed':'Get personal data by social network profile','g':'personal','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getRoleClassified','d':'Get Classified Role','ed':'Identify and classify a prospect role detecting the right area and seniority to filter later','g':'company','p':[{'n':'role','r':true,'t':'string','p':'Project Manager'}]},{'k':'checkCompanyDebtorByTaxid','d':'Check Company Is Debtor By TaxId (ES)','ed':'Check if company is debtor by TaxId','g':'company','p':[{'n':'taxid','r':true,'t':'string','p':'B04363115'}]},{'k':'getPersonDecisionMaker','d':'Get Decision Maker','ed':'Get professional data of a decision maker by company name/domain and area','g':'company','p':[{'n':'company','r':true,'t':'string','p':'uproc.io'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]}]},{'k':'getPersonDecisionMakerBySearch','d':'Get Decision Maker By Search Engine','ed':'Discover the more suitable decision maker using search engines (Bing) by company name and area (optional)','g':'company','p':[{'n':'company','r':true,'t':'string','p':'oracle'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Ecommerce','value':'Ecommerce'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]},{'n':'clevel','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]},{'n':'location','r':false,'t':'string','p':''},{'n':'keyword','r':false,'t':'string','p':''}]},{'k':'getCompanyDomainByName','d':'Get Domain By Company Name','ed':'Get company domain by company name','g':'company','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getPersonEmailsByDomainAndArea','d':'Get Decision Maker\'s Emails By Domain And Area','ed':'Get professional emails of decision makers (by priority: executive, manager and directors) by company domain and area','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'oracle.com'},{'n':'area','r':true,'t':'options','p':'Marketing','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]}]},{'k':'getCompanyExtendedByDomain','d':'Get Company (Extended) By Domain','ed':'Get company contact, social and technology data by domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'}]},{'k':'getCompanyExtendedByEmail','d':'Get Company (Extended) By Email','ed':'Get company contact, social and technology data by email','g':'company','p':[{'n':'email','r':true,'t':'string','p':'hello@killia.com'}]},{'k':'getPersonExtendedByProfile','d':'Get Person (Extended) By Profile','ed':'Get personal and social data by social profile','g':'personal','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getProfileFacebookByCompany','d':'Get Facebook URI By Company','ed':'Get Facebook company profile by name without manual search on Google or Facebook.

This tool uses search engines (Bing and Google) through proxies','g':'company','p':[{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getCompanyFinancialByDomain','d':'Get Sales Data By Company\'s Domain','ed':'Get company sales data by company\'s domain name.

Next countries are supported: Spain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getCompanyFinancialByDuns','d':'Get Sales Data By Company\'s DUNS','ed':'Get company sales data by company\'s DUNS number.

Next countries are supported: Spain','g':'company','p':[{'n':'duns','r':true,'t':'string','p':'461809423'}]},{'k':'getCompanyFinancialByName','d':'Get Sales Data By Company\'s Name','ed':'Get company sales data by company\'s name.

Next countries are supported: Spain','g':'company','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick S.L.'}]},{'k':'getCompanyFinancialByTaxid','d':'Get Sales Data By Company\'s Taxid','ed':'Get company sales data by company\'s taxid (CIF).

Next countries are supported: Spain','g':'company','p':[{'n':'taxid','r':true,'t':'string','p':'B62084959'}]},{'k':'getCompanyGeocodedByIp','d':'Get Geocoded Company By IP','ed':'Get geocoded company data by IP address','g':'company','p':[{'n':'ip','r':true,'t':'string','p':'74.125.228.72'}]},{'k':'sendLinkedinInvitation','d':'Send Connection Request To Linkedin Profile','ed':'Send a custom message invitation to a non connected Linkedin profile (supported uris: https://www.linkedin.com/in/USERID)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'message','r':true,'t':'string','p':'Hi {{first}}, would you like to be part of my network! Thanks'}]},{'k':'sendLinkedinInvitationOrMessage','d':'Send Invitation Or Message To Linkedin Profile','ed':'Send a custom invitation message (parameter message1) if profile is connected or a custom message (parameter message2) otherwise (supported uris: https://www.linkedin.com/in/USERID)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'message1','r':true,'t':'string','p':'Hi {{first}}, would you like to be part of my network? Thanks'},{'n':'message2','r':true,'t':'string','p':'Hi {{first}}, thank you for accepting my invitation!'}]},{'k':'getProfileLinkedinByCompany','d':'Get LinkedIn URI By Company','ed':'Get LinkedIn company profile by name without manual search on Google or LinkedIn.

This tool uses search engines (Bing and Google) through proxies','g':'company','p':[{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getPersonListByParams','d':'Get Employees By Parameters','ed':'Get employees by company name or domain, area, seniority and country','g':'company','p':[{'n':'country','r':true,'t':'options','p':'Spain','o':[{'name':'Afghanistan','value':'Afghanistan'},{'name':'Albania','value':'Albania'},{'name':'Algeria','value':'Algeria'},{'name':'American Samoa','value':'American Samoa'},{'name':'Andorra','value':'Andorra'},{'name':'Angola','value':'Angola'},{'name':'Anguilla','value':'Anguilla'},{'name':'Antarctica','value':'Antarctica'},{'name':'Antigua And Barbuda','value':'Antigua And Barbuda'},{'name':'Argentina','value':'Argentina'},{'name':'Armenia','value':'Armenia'},{'name':'Aruba','value':'Aruba'},{'name':'Australia','value':'Australia'},{'name':'Austria','value':'Austria'},{'name':'Azerbaijan','value':'Azerbaijan'},{'name':'Bahamas','value':'Bahamas'},{'name':'Bahrain','value':'Bahrain'},{'name':'Bangladesh','value':'Bangladesh'},{'name':'Barbados','value':'Barbados'},{'name':'Belarus','value':'Belarus'},{'name':'Belgium','value':'Belgium'},{'name':'Belize','value':'Belize'},{'name':'Benin','value':'Benin'},{'name':'Bermuda','value':'Bermuda'},{'name':'Bhutan','value':'Bhutan'},{'name':'Bolivia','value':'Bolivia'},{'name':'Bosnia And Herzegovina','value':'Bosnia And Herzegovina'},{'name':'Botswana','value':'Botswana'},{'name':'Bouvet Island','value':'Bouvet Island'},{'name':'Brazil','value':'Brazil'},{'name':'British Indian Ocean Territory','value':'British Indian Ocean Territory'},{'name':'Brunei Darussalam','value':'Brunei Darussalam'},{'name':'Bulgaria','value':'Bulgaria'},{'name':'Burkina Faso','value':'Burkina Faso'},{'name':'Burundi','value':'Burundi'},{'name':'Cambodia','value':'Cambodia'},{'name':'Cameroon','value':'Cameroon'},{'name':'Canada','value':'Canada'},{'name':'Cape Verde','value':'Cape Verde'},{'name':'Cayman Islands','value':'Cayman Islands'},{'name':'Central African Republic','value':'Central African Republic'},{'name':'Chad','value':'Chad'},{'name':'Chile','value':'Chile'},{'name':'China','value':'China'},{'name':'Christmas Island','value':'Christmas Island'},{'name':'Cocos (keeling) Islands','value':'Cocos (keeling) Islands'},{'name':'Colombia','value':'Colombia'},{'name':'Comoros','value':'Comoros'},{'name':'Congo','value':'Congo'},{'name':'Congo, The Democratic Republic Of The','value':'Congo, The Democratic Republic Of The'},{'name':'Cook Islands','value':'Cook Islands'},{'name':'Costa Rica','value':'Costa Rica'},{'name':'Cote D\'ivoire','value':'Cote D\'ivoire'},{'name':'Croatia','value':'Croatia'},{'name':'Cuba','value':'Cuba'},{'name':'Cyprus','value':'Cyprus'},{'name':'Czech Republic','value':'Czech Republic'},{'name':'Denmark','value':'Denmark'},{'name':'Djibouti','value':'Djibouti'},{'name':'Dominica','value':'Dominica'},{'name':'Dominican Republic','value':'Dominican Republic'},{'name':'East Timor','value':'East Timor'},{'name':'Ecuador','value':'Ecuador'},{'name':'Egypt','value':'Egypt'},{'name':'El Salvador','value':'El Salvador'},{'name':'Equatorial Guinea','value':'Equatorial Guinea'},{'name':'Eritrea','value':'Eritrea'},{'name':'Estonia','value':'Estonia'},{'name':'Ethiopia','value':'Ethiopia'},{'name':'Falkland Islands (malvinas)','value':'Falkland Islands (malvinas)'},{'name':'Faroe Islands','value':'Faroe Islands'},{'name':'Fiji','value':'Fiji'},{'name':'Finland','value':'Finland'},{'name':'France','value':'France'},{'name':'French Guiana','value':'French Guiana'},{'name':'French Polynesia','value':'French Polynesia'},{'name':'French Southern Territories','value':'French Southern Territories'},{'name':'Gabon','value':'Gabon'},{'name':'Gambia','value':'Gambia'},{'name':'Georgia','value':'Georgia'},{'name':'Germany','value':'Germany'},{'name':'Ghana','value':'Ghana'},{'name':'Gibraltar','value':'Gibraltar'},{'name':'Greece','value':'Greece'},{'name':'Greenland','value':'Greenland'},{'name':'Grenada','value':'Grenada'},{'name':'Guadeloupe','value':'Guadeloupe'},{'name':'Guam','value':'Guam'},{'name':'Guatemala','value':'Guatemala'},{'name':'Guinea','value':'Guinea'},{'name':'Guinea-bissau','value':'Guinea-bissau'},{'name':'Guyana','value':'Guyana'},{'name':'Haiti','value':'Haiti'},{'name':'Heard Island And Mcdonald Islands','value':'Heard Island And Mcdonald Islands'},{'name':'Holy See (vatican City State)','value':'Holy See (vatican City State)'},{'name':'Honduras','value':'Honduras'},{'name':'Hong Kong','value':'Hong Kong'},{'name':'Hungary','value':'Hungary'},{'name':'Iceland','value':'Iceland'},{'name':'India','value':'India'},{'name':'Indonesia','value':'Indonesia'},{'name':'Iran, Islamic Republic Of','value':'Iran, Islamic Republic Of'},{'name':'Iraq','value':'Iraq'},{'name':'Ireland','value':'Ireland'},{'name':'Israel','value':'Israel'},{'name':'Italy','value':'Italy'},{'name':'Jamaica','value':'Jamaica'},{'name':'Japan','value':'Japan'},{'name':'Jordan','value':'Jordan'},{'name':'Kazakstan','value':'Kazakstan'},{'name':'Kenya','value':'Kenya'},{'name':'Kiribati','value':'Kiribati'},{'name':'Korea, Democratic People\'s Republic Of','value':'Korea, Democratic People\'s Republic Of'},{'name':'Korea, Republic Of','value':'Korea, Republic Of'},{'name':'Kosovo','value':'Kosovo'},{'name':'Kuwait','value':'Kuwait'},{'name':'Kyrgyzstan','value':'Kyrgyzstan'},{'name':'Lao People\'s Democratic Republic','value':'Lao People\'s Democratic Republic'},{'name':'Latvia','value':'Latvia'},{'name':'Lebanon','value':'Lebanon'},{'name':'Lesotho','value':'Lesotho'},{'name':'Liberia','value':'Liberia'},{'name':'Libyan Arab Jamahiriya','value':'Libyan Arab Jamahiriya'},{'name':'Liechtenstein','value':'Liechtenstein'},{'name':'Lithuania','value':'Lithuania'},{'name':'Luxembourg','value':'Luxembourg'},{'name':'Macau','value':'Macau'},{'name':'Macedonia, The Former Yugoslav Republic Of','value':'Macedonia, The Former Yugoslav Republic Of'},{'name':'Madagascar','value':'Madagascar'},{'name':'Malawi','value':'Malawi'},{'name':'Malaysia','value':'Malaysia'},{'name':'Maldives','value':'Maldives'},{'name':'Mali','value':'Mali'},{'name':'Malta','value':'Malta'},{'name':'Marshall Islands','value':'Marshall Islands'},{'name':'Martinique','value':'Martinique'},{'name':'Mauritania','value':'Mauritania'},{'name':'Mauritius','value':'Mauritius'},{'name':'Mayotte','value':'Mayotte'},{'name':'Mexico','value':'Mexico'},{'name':'Micronesia, Federated States Of','value':'Micronesia, Federated States Of'},{'name':'Moldova, Republic Of','value':'Moldova, Republic Of'},{'name':'Monaco','value':'Monaco'},{'name':'Mongolia','value':'Mongolia'},{'name':'Montenegro','value':'Montenegro'},{'name':'Montserrat','value':'Montserrat'},{'name':'Morocco','value':'Morocco'},{'name':'Mozambique','value':'Mozambique'},{'name':'Myanmar','value':'Myanmar'},{'name':'Namibia','value':'Namibia'},{'name':'Nauru','value':'Nauru'},{'name':'Nepal','value':'Nepal'},{'name':'Netherlands','value':'Netherlands'},{'name':'Netherlands Antilles','value':'Netherlands Antilles'},{'name':'New Caledonia','value':'New Caledonia'},{'name':'New Zealand','value':'New Zealand'},{'name':'Nicaragua','value':'Nicaragua'},{'name':'Niger','value':'Niger'},{'name':'Nigeria','value':'Nigeria'},{'name':'Niue','value':'Niue'},{'name':'Norfolk Island','value':'Norfolk Island'},{'name':'Northern Mariana Islands','value':'Northern Mariana Islands'},{'name':'Norway','value':'Norway'},{'name':'Oman','value':'Oman'},{'name':'Pakistan','value':'Pakistan'},{'name':'Palau','value':'Palau'},{'name':'Palestinian Territory, Occupied','value':'Palestinian Territory, Occupied'},{'name':'Panama','value':'Panama'},{'name':'Papua New Guinea','value':'Papua New Guinea'},{'name':'Paraguay','value':'Paraguay'},{'name':'Peru','value':'Peru'},{'name':'Philippines','value':'Philippines'},{'name':'Pitcairn','value':'Pitcairn'},{'name':'Poland','value':'Poland'},{'name':'Portugal','value':'Portugal'},{'name':'Puerto Rico','value':'Puerto Rico'},{'name':'Qatar','value':'Qatar'},{'name':'Reunion','value':'Reunion'},{'name':'Romania','value':'Romania'},{'name':'Russian Federation','value':'Russian Federation'},{'name':'Rwanda','value':'Rwanda'},{'name':'Saint Helena','value':'Saint Helena'},{'name':'Saint Kitts And Nevis','value':'Saint Kitts And Nevis'},{'name':'Saint Lucia','value':'Saint Lucia'},{'name':'Saint Pierre And Miquelon','value':'Saint Pierre And Miquelon'},{'name':'Saint Vincent And The Grenadines','value':'Saint Vincent And The Grenadines'},{'name':'Samoa','value':'Samoa'},{'name':'San Marino','value':'San Marino'},{'name':'Sao Tome And Principe','value':'Sao Tome And Principe'},{'name':'Saudi Arabia','value':'Saudi Arabia'},{'name':'Senegal','value':'Senegal'},{'name':'Serbia','value':'Serbia'},{'name':'Seychelles','value':'Seychelles'},{'name':'Sierra Leone','value':'Sierra Leone'},{'name':'Singapore','value':'Singapore'},{'name':'Slovakia','value':'Slovakia'},{'name':'Slovenia','value':'Slovenia'},{'name':'Solomon Islands','value':'Solomon Islands'},{'name':'Somalia','value':'Somalia'},{'name':'South Africa','value':'South Africa'},{'name':'South Georgia And The South Sandwich Islands','value':'South Georgia And The South Sandwich Islands'},{'name':'Spain','value':'Spain'},{'name':'Sri Lanka','value':'Sri Lanka'},{'name':'Sudan','value':'Sudan'},{'name':'Suriname','value':'Suriname'},{'name':'Svalbard And Jan Mayen','value':'Svalbard And Jan Mayen'},{'name':'Swaziland','value':'Swaziland'},{'name':'Sweden','value':'Sweden'},{'name':'Switzerland','value':'Switzerland'},{'name':'Syrian Arab Republic','value':'Syrian Arab Republic'},{'name':'Taiwan, Province Of China','value':'Taiwan, Province Of China'},{'name':'Tajikistan','value':'Tajikistan'},{'name':'Tanzania, United Republic Of','value':'Tanzania, United Republic Of'},{'name':'Thailand','value':'Thailand'},{'name':'Togo','value':'Togo'},{'name':'Tokelau','value':'Tokelau'},{'name':'Tonga','value':'Tonga'},{'name':'Trinidad And Tobago','value':'Trinidad And Tobago'},{'name':'Tunisia','value':'Tunisia'},{'name':'Turkey','value':'Turkey'},{'name':'Turkmenistan','value':'Turkmenistan'},{'name':'Turks And Caicos Islands','value':'Turks And Caicos Islands'},{'name':'Tuvalu','value':'Tuvalu'},{'name':'Uganda','value':'Uganda'},{'name':'Ukraine','value':'Ukraine'},{'name':'United Arab Emirates','value':'United Arab Emirates'},{'name':'United Kingdom','value':'United Kingdom'},{'name':'United States','value':'United States'},{'name':'United States Minor Outlying Islands','value':'United States Minor Outlying Islands'},{'name':'Uruguay','value':'Uruguay'},{'name':'Uzbekistan','value':'Uzbekistan'},{'name':'Vanuatu','value':'Vanuatu'},{'name':'Venezuela','value':'Venezuela'},{'name':'Viet Nam','value':'Viet Nam'},{'name':'Virgin Islands, British','value':'Virgin Islands, British'},{'name':'Virgin Islands, U.s.','value':'Virgin Islands, U.s.'},{'name':'Wallis And Futuna','value':'Wallis And Futuna'},{'name':'Western Sahara','value':'Western Sahara'},{'name':'Yemen','value':'Yemen'},{'name':'Zambia','value':'Zambia'},{'name':'Zimbabwe','value':'Zimbabwe'}]},{'n':'company','r':false,'t':'string','p':'oracle'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]},{'n':'seniority','r':false,'t':'options','p':'','o':[{'name':'Apprentice','value':'Apprentice'},{'name':'Director','value':'Director'},{'name':'Executive','value':'Executive'},{'name':'Intermediate','value':'Intermediate'},{'name':'Manager','value':'Manager'}]},{'n':'email','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]},{'n':'phone','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]}]},{'k':'sendLinkedinMessage','d':'Send Message To Linkedin Profile','ed':'Send a custom private message to a connected Linkedin profile (supported uris: https://www.linkedin.com/in/USERID)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'message','r':true,'t':'string','p':'Hi {{first}}, would you like to be part of my network? Thanks'}]},{'k':'getPersonMultipleDecisionMakerBySearch','d':'Get Decision Makers By Search Engine','ed':'Discover up to ten decision makers using search engines (Bing) by company name and area (optional)','g':'company','p':[{'n':'company','r':true,'t':'string','p':'oracle'},{'n':'area','r':false,'t':'options','p':'','o':[{'name':'Communications','value':'Communications'},{'name':'Consulting','value':'Consulting'},{'name':'Customer service','value':'Customer service'},{'name':'Ecommerce','value':'Ecommerce'},{'name':'Education','value':'Education'},{'name':'Engineering','value':'Engineering'},{'name':'Finance','value':'Finance'},{'name':'Health professional','value':'Health professional'},{'name':'Human resources','value':'Human resources'},{'name':'Information technology','value':'Information technology'},{'name':'Legal','value':'Legal'},{'name':'Marketing','value':'Marketing'},{'name':'Operations','value':'Operations'},{'name':'Owner','value':'Owner'},{'name':'President','value':'President'},{'name':'Product','value':'Product'},{'name':'Public relations','value':'Public relations'},{'name':'Real estate','value':'Real estate'},{'name':'Recruiting','value':'Recruiting'},{'name':'Research','value':'Research'},{'name':'Sales','value':'Sales'}]},{'n':'clevel','r':false,'t':'options','p':'','o':[{'name':'No','value':'No'},{'name':'Yes','value':'Yes'}]},{'n':'location','r':false,'t':'string','p':''},{'n':'keyword','r':false,'t':'string','p':''}]},{'k':'getCompanyNameByDomain','d':'Get Company\'s Name By Domain','ed':'Get company name by company domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getCompanyPhoneByDomain','d':'Get Phone By Company Domain','ed':'Get company phone by company domain','g':'company','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getCompanyPhoneByName','d':'Get Phone By Company Name','ed':'Get company phone by company name','g':'company','p':[{'n':'name','r':true,'t':'string','p':'cyberclick'}]},{'k':'getProfileTwitterByCompany','d':'Get Twitter URI By Company','ed':'Get Twitter company profile by name without manual search on Google or Twitter.

This tool uses search engines (Bing and Google) through proxies','g':'company','p':[{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getDistanceByAddresses','d':'Get Distance Between Addresses','ed':'Returns straight-line distance in kilometers between two addresses','g':'geographic','p':[{'n':'address1','r':true,'t':'string','p':'Mallorca 120, 08036, Barcelona'},{'n':'address2','r':true,'t':'string','p':'Mallorca 70, 08036, Barcelona'}]},{'k':'getRouteByAddresses','d':'Get Route Between Addresses','ed':'Returns driving routing time, distance, fuel consumption and cost between two addresses','g':'geographic','p':[{'n':'address1','r':true,'t':'string','p':'Mallorca 120, 08036, Barcelona'},{'n':'address2','r':true,'t':'string','p':'Mallorca 70, 08036, Barcelona'},{'n':'fuel_consumption','r':true,'t':'string','p':'5'},{'n':'price_liter','r':true,'t':'string','p':'1.2'}]},{'k':'getDistanceByCoordinates','d':'Get Distance Between Coordinates','ed':'Returns straight-line distance in kilometers between two GPS coordinates (latitude and longitude)','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.30'}]},{'k':'getRouteByCoordinates','d':'Get Route Between Coordinates','ed':'Returns driving routing time, distance, fuel consumption and cost between two GPS coordinates (latitude and longitude)','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.30'}]},{'k':'getCoordinateByIp','d':'Get Coordinates By IP','ed':'Discover latitude and longitude coordinates of an IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getDistanceByIps','d':'Get Distance Between IPs','ed':'Returns straight-line distance in kilometers between two IP addresses','g':'geographic','p':[{'n':'ip1','r':true,'t':'string','p':'95.23.100.79'},{'n':'ip2','r':true,'t':'string','p':'88.190.16.36'}]},{'k':'getRouteByIps','d':'Get Route Between IPs','ed':'Returns driving routing time, distance, fuel consumption and cost between two IP addresses','g':'geographic','p':[{'n':'ip1','r':true,'t':'string','p':'95.23.100.79'},{'n':'ip2','r':true,'t':'string','p':'88.190.16.36'},{'n':'fuel_consumption','r':true,'t':'string','p':'5'},{'n':'price_liter','r':true,'t':'string','p':'1.2'}]},{'k':'getDistanceByPhones','d':'Get Distance Between Phones','ed':'Returns straight-line distance in kilometers between two landline phones, using city and province of every phone','g':'geographic','p':[{'n':'phone1','r':true,'t':'string','p':'932187670'},{'n':'phone2','r':true,'t':'string','p':'91213111111'}]},{'k':'getRouteByPhones','d':'Get Route Between Phones','ed':'Returns driving routing time, distance, fuel consumption and cost between two landline phones, using city and province of every phone (only Spain)','g':'geographic','p':[{'n':'phone1','r':true,'t':'string','p':'932187670'},{'n':'phone2','r':true,'t':'string','p':'91213111111'}]},{'k':'getDistanceByZipcodes','d':'Get Distance Between Zipcodes','ed':'Returns straight-line distance in kilometers between two zipcodes, using city and province of every zipcode','g':'geographic','p':[{'n':'zipcode1','r':true,'t':'string','p':'08012'},{'n':'zipcode2','r':true,'t':'string','p':'28080'}]},{'k':'getRouteByZipcodes','d':'Get Route Between Zipcodes','ed':'Returns driving routing time, distance, fuel consumption and cost between two zipcodes, using city and province of every zipcode','g':'geographic','p':[{'n':'zipcode1','r':true,'t':'string','p':'08012'},{'n':'zipcode2','r':true,'t':'string','p':'28080'}]},{'k':'getCoordinateCartesian','d':'Get Cartesian Coordinates','ed':'Get Cartesian coordinates (X,Y,Z/WGS84) by Latitude and Longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'getCoordinateDecimal','d':'Get Decimal Coordinates','ed':'Get Decimal coordinates (degrees, minutes and seconds) by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'checkDistanceEq','d':'Check Distance Is Equal','ed':'Discover if the distance between two coordinates is equal to another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'33.44'}]},{'k':'checkDistanceGe','d':'Check Distance Is Greater Or Equal','ed':'Discover if the distance in quilometers between two coordinates is greater or equal than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'33'}]},{'k':'checkDistanceGt','d':'Check Distance Is Greater','ed':'Discover if the distance in quilometers between two coordinates is greater than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'30'}]},{'k':'checkDistanceLe','d':'Check Distance Is Lower Or Equal','ed':'Discover if the distance in quilometers between two coordinates is lower or equal than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'34'}]},{'k':'checkDistanceLt','d':'Check Distance Is Lower','ed':'Discover if the distance in quilometers between two coordinates is lower than another','g':'geographic','p':[{'n':'coordinates1','r':true,'t':'string','p':'41.2522,-12.10'},{'n':'coordinates2','r':true,'t':'string','p':'41.2522,-12.50'},{'n':'distance','r':true,'t':'string','p':'50'}]},{'k':'getCoordinateUsng','d':'Get USNG Coordinates','ed':'Get USNG coordinates by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'getCoordinateUtm','d':'Get UTM Coordinates','ed':'Get UTM coordinates by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.38879,2.15899'}]},{'k':'getCountryByCode','d':'Get Country By ISO Code','ed':'Get country name by its ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getCountryByCurrencyCode','d':'Get Country By Currency','ed':'Get country name by currency ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'getCountryByIp','d':'Get Country By IP','ed':'Get country name by IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCountryByName','d':'Get Country By Name','ed':'Get country by prefix','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'Spa'}]},{'k':'getCountryByPhone','d':'Get Country By Phone','ed':'Get country name by phone number, with worldwide coverage','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'57122000111'}]},{'k':'getCountryCodeByName','d':'Get Country Code By Name','ed':'Get Alpha2 code by country prefix or name','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'Spain'}]},{'k':'getCountryListByCode','d':'Get Countries By ISO Code','ed':'Get multiple countries by ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getCountryListByCurrencyCode','d':'Get Countries By Currency','ed':'Get multiple country names by currency ISO code','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'getCountryListByName','d':'Get Countries By Prefix','ed':'Get multiple country names by initial name','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'S'}]},{'k':'getCountryNormalized','d':'Get Normalized Country','ed':'Allow to normalize a country, removing non allowed characters','g':'geographic','p':[{'n':'country','r':true,'t':'string','p':'Spa1n'}]},{'k':'checkCountryValidIso','d':'Check Country Code Exists','ed':'Check if country ISO code exists','g':'geographic','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getCurrencyByCountry','d':'Get Currency Code By Country Name','ed':'Get ISO currency code by a country name','g':'finance','p':[{'n':'country','r':true,'t':'string','p':'Spain'}]},{'k':'getCurrencyByCountryIsocode','d':'Get Currency Code By Country Code','ed':'Get ISO currency code by an ISO country code','g':'finance','p':[{'n':'country_code','r':true,'t':'string','p':'ES'}]},{'k':'getCurrencyByIp','d':'Get Currency By IP','ed':'Get ISO currency code by IP address','g':'finance','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCurrencyByIsocode','d':'Get Currency By ISO Currency Code','ed':'Get an ISO currency code by a currency ISO code','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'getCurrencyConvertedBetweenIsocodeDate','d':'Get Conversion By Currencies And Date','ed':'Convert amount between supported currencies and an exchange date','g':'finance','p':[{'n':'amount','r':true,'t':'string','p':'10'},{'n':'isocode1','r':true,'t':'options','p':'EUR','o':[{'name':'AUD','value':'AUD'},{'name':'BGN','value':'BGN'},{'name':'BRL','value':'BRL'},{'name':'CAD','value':'CAD'},{'name':'CHF','value':'CHF'},{'name':'CNY','value':'CNY'},{'name':'CZK','value':'CZK'},{'name':'DKK','value':'DKK'},{'name':'EUR','value':'EUR'},{'name':'GBP','value':'GBP'},{'name':'HKD','value':'HKD'},{'name':'HRK','value':'HRK'},{'name':'HUF','value':'HUF'},{'name':'IDR','value':'IDR'},{'name':'ILS','value':'ILS'},{'name':'INR','value':'INR'},{'name':'ISK','value':'ISK'},{'name':'JPY','value':'JPY'},{'name':'KRW','value':'KRW'},{'name':'MXN','value':'MXN'},{'name':'MYR','value':'MYR'},{'name':'NOK','value':'NOK'},{'name':'NZD','value':'NZD'},{'name':'PHP','value':'PHP'},{'name':'PLN','value':'PLN'},{'name':'RON','value':'RON'},{'name':'RUB','value':'RUB'},{'name':'SEK','value':'SEK'},{'name':'SGD','value':'SGD'},{'name':'THB','value':'THB'},{'name':'TRY','value':'TRY'},{'name':'USD','value':'USD'},{'name':'ZAR','value':'ZAR'}]},{'n':'isocode2','r':true,'t':'options','p':'USD','o':[{'name':'AUD','value':'AUD'},{'name':'BGN','value':'BGN'},{'name':'BRL','value':'BRL'},{'name':'CAD','value':'CAD'},{'name':'CHF','value':'CHF'},{'name':'CNY','value':'CNY'},{'name':'CZK','value':'CZK'},{'name':'DKK','value':'DKK'},{'name':'EUR','value':'EUR'},{'name':'GBP','value':'GBP'},{'name':'HKD','value':'HKD'},{'name':'HRK','value':'HRK'},{'name':'HUF','value':'HUF'},{'name':'IDR','value':'IDR'},{'name':'ILS','value':'ILS'},{'name':'INR','value':'INR'},{'name':'ISK','value':'ISK'},{'name':'JPY','value':'JPY'},{'name':'KRW','value':'KRW'},{'name':'MXN','value':'MXN'},{'name':'MYR','value':'MYR'},{'name':'NOK','value':'NOK'},{'name':'NZD','value':'NZD'},{'name':'PHP','value':'PHP'},{'name':'PLN','value':'PLN'},{'name':'RON','value':'RON'},{'name':'RUB','value':'RUB'},{'name':'SEK','value':'SEK'},{'name':'SGD','value':'SGD'},{'name':'THB','value':'THB'},{'name':'TRY','value':'TRY'},{'name':'USD','value':'USD'},{'name':'ZAR','value':'ZAR'}]},{'n':'date','r':false,'t':'string','p':'2018-02-12'}]},{'k':'getCurrencyListByCountry','d':'Get Currencies By Country','ed':'Get multiple ISO currency codes by a country name','g':'finance','p':[{'n':'country','r':true,'t':'string','p':'Spain'}]},{'k':'getCurrencyListByIp','d':'Get Currencies By IP','ed':'Get all ISO currency codes by an IP address','g':'finance','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getCurrencyListByIsocode','d':'Get Currencies By ISO Code','ed':'Get multiple ISO currency codes by a country ISO code','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'checkCurrencyValidIso','d':'Check ISO Currency Code Is Valid','ed':'Discover if an ISO currency code is valid','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'EUR'}]},{'k':'checkDateBetw','d':'Check Date Is Between Dates','ed':'Discover if a date (date1) is betwen two dates (date2, date3)','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-20'},{'n':'date2','r':true,'t':'string','p':'1975-05-19'},{'n':'date3','r':true,'t':'string','p':'1975-05-22'}]},{'k':'getDateDifference','d':'Get Difference Between Dates','ed':'Returns difference between two dates (start and end) in seconds, minutes, hours or days
By default, difference is returned in seconds.','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'2018-10-10 00:00:00'},{'n':'date2','r':true,'t':'string','p':'2018-10-11 00:00:00'},{'n':'period','r':true,'t':'options','p':'seconds','o':[{'name':'Days','value':'days'},{'name':'Hours','value':'hours'},{'name':'Minutes','value':'minutes'},{'name':'Seconds','value':'seconds'}]}]},{'k':'checkDateEq','d':'Check Dates Are Equal','ed':'Discover if two dates are equal','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-20'},{'n':'date2','r':false,'t':'string','p':'1975-05-20'}]},{'k':'checkDateGe','d':'Check Date Is Greater Or Equal','ed':'Discover if a date is greater or equal','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-22'},{'n':'date2','r':false,'t':'string','p':'1975-05-21'}]},{'k':'checkDateGt','d':'Check Date Is Greater','ed':'Discover if a date is greater','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-22'},{'n':'date2','r':false,'t':'string','p':'1975-05-21'}]},{'k':'checkDateLe','d':'Check Date Is Lower Or Equal','ed':'Discover if a date is lower or equal','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-19'},{'n':'date2','r':false,'t':'string','p':'1975-05-20'}]},{'k':'checkDateLeap','d':'Check Date Is Leap Year','ed':'Discover if a date belongs to a leap year','g':'personal','p':[{'n':'date','r':false,'t':'string','p':'2008-05-20'}]},{'k':'checkDateLt','d':'Check Date Is Lower','ed':'Discover if a date is lower','g':'personal','p':[{'n':'date1','r':true,'t':'string','p':'1975-05-20'},{'n':'date2','r':false,'t':'string','p':'1975-05-21'}]},{'k':'getDateNormalized','d':'Get Normalized Date','ed':'Allow to normalize a date, removing non allowed characters','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'20 /01/2018'}]},{'k':'getDateParsed','d':'Get Parsed Datetime','ed':'Parse datetime, without format dependency, into multiple fields','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'20/01/2018'}]},{'k':'checkDateValid','d':'Check Date Is Valid','ed':'Discover if a date has a valid format','g':'personal','p':[{'n':'date','r':true,'t':'string','p':'1975-05-20'}]},{'k':'getDeviceByUa','d':'Get Device By User Agent','ed':'Discover device features by user agent','g':'internet','p':[{'n':'useragent','r':true,'t':'string','p':'AppleTV5,3/9.1.1'}]},{'k':'getDomainBlacklists','d':'Get Domain Blacklists','ed':'Get all blacklists where a domain appears','g':'security','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getUrlByDomain','d':'Get Default URL By Domain','ed':'Get valid, existing and default URL when accessing a domain using a web browser.','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'getDomainByIp','d':'Get Domain By IP','ed':'Get the domain name related with the IP address','g':'internet','p':[{'n':'ip','r':true,'t':'string','p':'212.85.34.20'}]},{'k':'getDomainByUrl','d':'Get Root Domain By Web Address','ed':'Get root domain of any web address, removing non needed characters.','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.uproc.io'}]},{'k':'checkDomainCatchall','d':'Check Email Domain Is Catchall','ed':'Check if domain accepts all emails, existing or not','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'abinitio.es'}]},{'k':'checkDomainCertificate','d':'Check Domain Has Valid Certificate','ed':'Check if domain has a valid SSL certificate','g':'internet','p':[{'n':'domain','r':false,'t':'string','p':'uproc.io'}]},{'k':'getDomainCertificate','d':'Get Certificate By Domain','ed':'Get full SSL certificate data by domain (or website) and monitor your certificate status.

If domain has port 443 opened, a response will be returned.','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'checkDomainDisposable','d':'Check Email Domain Is Temporary','ed':'Check if domain is temporary or not','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cowstore.org'}]},{'k':'checkDomainExist','d':'Check Domain Exists','ed':'Check if domain exists','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'mydomain.com'}]},{'k':'checkDomainFormat','d':'Check Domain Has Valid Format','ed':'Check if domain has a valid format','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'checkDomainFree','d':'Check Email Domain Is Free','ed':'Check if domain is a free service domain provider','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getDomainIsp','d':'Get ISP By Domain','ed':'Get ISP known name of email domain name (hotmail, yahoo, gmail, mailgun, zoho, other)','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getDomainLogo','d':'Get Logo By Domain','ed':'Discover logo (favicon) used in domain','g':'image','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'checkDomainMx','d':'Check Domain Has MX Record','ed':'Check if domain has a MX record','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'getUrlPdf','d':'Get Pdf By URL','ed':'Generate a PDF file by URL provided using Chrome browser','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'}]},{'k':'checkDomainRecord','d':'Check Domain Has DNS Record','ed':'Check if domain has a record of that type','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'mydomain.com'},{'n':'type','r':false,'t':'options','p':'A','o':[{'name':'A','value':'A'},{'name':'AAAA','value':'AAAA'},{'name':'CNAME','value':'CNAME'},{'name':'MX','value':'MX'},{'name':'NS','value':'NS'},{'name':'TXT','value':'TXT'}]}]},{'k':'getDomainRecord','d':'Get Domain Record By DNS Type','ed':'Get the domain record by its type','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'},{'n':'type','r':false,'t':'options','p':'A','o':[{'name':'A','value':'A'},{'name':'AAAA','value':'AAAA'},{'name':'CNAME','value':'CNAME'},{'name':'MX','value':'MX'},{'name':'NS','value':'NS'},{'name':'TXT','value':'TXT'}]}]},{'k':'getDomainRecords','d':'Get Domain DNS Records','ed':'Get all domain dns records','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'google.es'}]},{'k':'checkDomainReverse','d':'Check Domain Has IP','ed':'Check if domain has assigned the IP address defined','g':'internet','p':[{'n':'domain','r':false,'t':'string','p':'mail.nova.es'},{'n':'ip','r':true,'t':'string','p':'212.85.34.20'}]},{'k':'getDomainReverseIp','d':'Get IP By Domain','ed':'Get the IPv4 address linked with a domain','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'www.gmail.com'}]},{'k':'getUrlScreenshot','d':'Get Screenshot By URL','ed':'Generate a screenshot by URL provided using Chrome browser','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'},{'n':'width','r':false,'t':'options','p':'640','o':[{'name':'1024','value':'1024'},{'name':'160','value':'160'},{'name':'320','value':'320'},{'name':'640','value':'640'},{'name':'800','value':'800'}]},{'n':'fullpage','r':false,'t':'options','p':'no','o':[{'name':'No','value':'no'},{'name':'Yes','value':'yes'}]},{'n':'useragent','r':false,'t':'string','p':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36'},{'n':'selector','r':false,'t':'string','p':'h1.mt-4'}]},{'k':'getUrlShareableLinks','d':'Get Shareable Links','ed':'Generates shareable URIs to use on social networks and email using a content URI and a text.','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.uproc.io/'},{'n':'text','r':true,'t':'string','p':'This is an amazing title for my content'}]},{'k':'getDomainTechnologies','d':'Get Technologies By Domain','ed':'Discover client and server technologies used in domain','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'getUrlTechnologies','d':'Get Technologies By URL','ed':'Discover client and server technologies used in web page','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.uproc.io/'}]},{'k':'getDomainVisits','d':'Get Visits By Domain','ed':'Get Website visits and rank of any domain','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'gmail.com'}]},{'k':'getDomainWhois','d':'Get Whois By Domain','ed':'Get the domain whois data by fields','g':'internet','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'}]},{'k':'getIpWhois','d':'Get Whois By IP Address','ed':'Get whois data fields by IP address provided.','g':'internet','p':[{'n':'ip','r':true,'t':'string','p':'140.82.118.4'}]},{'k':'sendEmailCustom','d':'Send Custom Email','ed':'Send a custom email (HTML supported) to a recipient','g':'communication','p':[{'n':'email_from','r':true,'t':'string','p':'mcolomer@killia.com'},{'n':'email_to','r':true,'t':'string','p':'mcolomer@gmail.com'},{'n':'subject','r':true,'t':'string','p':'Welcome email'},{'n':'body','r':true,'t':'string','p':'Hi!

Welcome to uProc and start improving your business processes!'}]},{'k':'checkEmailDisposable','d':'Check Email Is Disposable Domain','ed':'Check if email domain belongs to a disposable email service','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'info@jetable.com'}]},{'k':'getEmailDomain','d':'Get Domain By Email','ed':'Get domain part from an email','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkEmailExists','d':'Check Email Exists (Simple)','ed':'Discover if the email recipient exists, returning email status','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkEmailExistsExtended','d':'Check Email Exists (Extended)','ed':'Discover if an email is valid, hardbounce, softbounce, spamtrap, free, temporary and recipient exists.

There are catchall (like Yahoo) or temporary domains that do not return the actual existence of an email','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getEmailFirstReferences','d':'Get First Web References Of An Email','ed':'Get three first web references of an email published on Internet','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'mcolomer@killia.com'}]},{'k':'getEmailFix','d':'Get Fixed Domain Email','ed':'Fix the email domain of those misspelled emails (supports all domains)','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test@gmil.com'}]},{'k':'checkEmailFormat','d':'Check Email Has Valid Format','ed':'Check if email has a valid format','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test@test.com'}]},{'k':'checkEmailFree','d':'Check Email Is Free','ed':'Check if email belongs to free service provider, like gmail, hotmail, ...','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'info@gmail.com'}]},{'k':'getEmailGdprListByDomain','d':'Get GDPR Emails By Domain','ed':'Get GDPR compliant emails list by domain for your Email Marketing campaigns in Europe.','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'}]},{'k':'getEmailListByDomain','d':'Get Emails By Domain','ed':'Get emails list found on internet by domain or URI (similar to hunter.io)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'uproc.io'},{'n':'page','r':false,'t':'number','p':'1'}]},{'k':'getEmailListByEmail','d':'Get Emails By Email','ed':'Get emails list found on internet by non-free email (similar to hunter.io)','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'},{'n':'page','r':false,'t':'number','p':'1'}]},{'k':'getEmailListInSite','d':'Get Emails In Website','ed':'Get emails list found inside website by domain or URI (similar to hunter.io)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'uoc.edu'},{'n':'page','r':false,'t':'number','p':'1'}]},{'k':'getEmailNormalized','d':'Get Normalized Email','ed':'Normalize email address, removing non allowed characters','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test @gmail.com'}]},{'k':'getEmailRecipient','d':'Get Email By Name, Surname And Domain','ed':'Discover an email by company website or domain and prospect\'s firstname and lastname.

If \'verify\' method is selected, tool checks multiple email variants in real-time, and returns a result depending on email server response','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'},{'n':'firstname','r':true,'t':'string','p':'Miquel'},{'n':'lastname','r':false,'t':'string','p':'Colomer'},{'n':'mode','r':true,'t':'options','p':'guess','o':[{'name':'Guess','value':'guess'},{'name':'Verify','value':'verify'}]}]},{'k':'getEmailRecipientByCompanyAndFullname','d':'Get Email By Fullname And Company','ed':'Discover an email by company\'s name and prospect\'s fullname.

If \'verify\' method is selected, tool checks multiple email variants in real-time, and returns a result depending on email server response','g':'communication','p':[{'n':'company','r':true,'t':'string','p':'uproc'},{'n':'fullname','r':true,'t':'string','p':'Miquel Colomer'},{'n':'mode','r':true,'t':'options','p':'guess','o':[{'name':'Guess','value':'guess'},{'name':'Verify','value':'verify'}]}]},{'k':'getEmailRecipientByDomainAndFullname','d':'Get Email By Fullname And Domain','ed':'Discover an email by company website or domain and prospect\'s fullname.

If \'verify\' method is selected, tool checks multiple email variants in real-time, and returns a result depending on email server response','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'},{'n':'fullname','r':true,'t':'string','p':'Miquel Colomer'},{'n':'mode','r':true,'t':'options','p':'guess','o':[{'name':'Guess','value':'guess'},{'name':'Verify','value':'verify'}]}]},{'k':'getEmailRecipientByProfile','d':'Get Email By Social Network Profile','ed':'Discover an email by contact\'s LinkedIn profile URI','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getEmailRecipientGdpr','d':'Get Public Email By Name, Surname And Domain (GDPR)','ed':'Discover an email by company website or domain and prospect\'s firstname and lastname.

The tool only uses publicly available emails found on the internet and matches the recipient by first name and last name (GDPR compliant)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'killia.com'},{'n':'firstname','r':true,'t':'string','p':'hello'},{'n':'lastname','r':false,'t':'string','p':''}]},{'k':'getEmailReferences','d':'Get Web References Of An Email','ed':'Get web references of an email published on Internet','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'mcolomer@killia.com'}]},{'k':'checkEmailRole','d':'Check Email Is Role Based','ed':'Check if email belongs to a system or role based account','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'test@gmail.com'}]},{'k':'sendMobileSms','d':'Send Custom Sms','ed':'Send a custom sms to a recipient with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'},{'n':'text','r':true,'t':'string','p':'Thanks for your participation!'}]},{'k':'checkEmailSmtp','d':'Check Email Has SMTP Server','ed':'Check if email domain has an SMTP server to receive emails','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkEmailSpamtrap','d':'Check Email Is Spam Trap','ed':'Check if email is a spam trap','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'zzzwuzhdgvrxy@yahoo.co.jp'}]},{'k':'getEmailType','d':'Get Email Type','ed':'Get email type','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getFileCopiedBetweenUrls','d':'Get File Copied Between URLs','ed':'Copy file from one URL to another URL','g':'internet','p':[{'n':'source','r':true,'t':'string','p':'https://habilon.com/wp-content/uploads/2019/09/11b07c01.pdf'},{'n':'destination','r':true,'t':'string','p':'s3://ACCESSKEY:SECRETKEY@s3.amazon.com/BUCKET_NAME/sample.pdf'}]},{'k':'getFullnameParsed','d':'Get Parsed Fullname','ed':'Normalize fullname, fixing abbreviations, sorting if necessary and returning firstname, lastname and gender','g':'personal','p':[{'n':'fullname','r':true,'t':'string','p':'Colomer Salas Miquel'}]},{'k':'getGenderByEmail','d':'Get Gender By Email','ed':'Discover the gender of a person by the email, if person name is included in email','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel.colomer@gmail.com'}]},{'k':'getGenderByPersonalName','d':'Get Gender By Name','ed':'Discover the gender of a person or company by name','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Marc'}]},{'k':'checkGenderValid','d':'Check Gender Is Valid','ed':'Discover if a gender value is valid (multilanguage)','g':'personal','p':[{'n':'gender','r':true,'t':'string','p':'male'}]},{'k':'getImageExif','d':'Get EXIF Metadata From Image','ed':'It allows to discover all geograhical and technical EXIF metadata present in a photographic JPEG image.','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://killia-internal.s3-eu-west-1.amazonaws.com/sample/uproc_photo_with_exif.jpg'}]},{'k':'getImageWithText','d':'Get Image With Text','ed':'Generate a new image by URL and text','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://killia-internal.s3-eu-west-1.amazonaws.com/sample/uproc_sample_resized.jpg'},{'n':'text','r':true,'t':'string','p':'Hi Miquel!'},{'n':'size','r':false,'t':'string','p':'80'}]},{'k':'getQrDecoded','d':'Get Decoded QR Code','ed':'Get QR Code decoded content by an image URL','g':'image','p':[{'n':'url','r':true,'t':'string','p':'https://s3.amazonaws.com/any-file/qr_38efdf6c60074375a6b0061201c644ac.png'}]},{'k':'getQrEncoded','d':'Get Encoded QR Code','ed':'Get QR Code encoded by a text','g':'image','p':[{'n':'text','r':true,'t':'string','p':'Sample text to encode'}]},{'k':'getIpBlacklists','d':'Get Ip Blacklists','ed':'Get all blacklists where an IP address appears','g':'security','p':[{'n':'ip','r':true,'t':'string','p':'172.217.168.165'}]},{'k':'getLinkedinConnections','d':'Get LinkedIn Last Received Connections','ed':'Extract last 80 connections from your LinkedIn profile','g':'communication','p':[{'n':'list','r':false,'t':'string','p':'last-connections'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'}]}]},{'k':'getLinkedinGroupMembers','d':'Get LinkedIn Group Members','ed':'Get members in a LinkedIn group','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/groups/59923/members/'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getLinkedinInvitations','d':'Get LinkedIn Last Sent Invitations','ed':'Extract last 80 invitations sent from your LinkedIn','g':'communication','p':[{'n':'list','r':false,'t':'string','p':'last-invitations'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'}]}]},{'k':'getLinkedinPostComments','d':'Get LinkedIn Post Comments','ed':'Get users who comment a post on LinkedIn','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/posts/miquelcolomersalas_gdpr-emails-emailmarketing-activity-6607189465423314944-dbPv'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getLinkedinPostLikes','d':'Get LinkedIn Post Likes','ed':'Get users who like a post on LinkedIn','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/posts/miquelcolomersalas_gdpr-emails-emailmarketing-activity-6607189465423314944-dbPv'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getLinkedinProfile','d':'Get LinkedIn Profile','ed':'Extract a LinkedIn profile (url format accepted: https://linkedin.com/in/USERNAME)','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'},{'n':'list','r':false,'t':'string','p':'my-list'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'},{'name':'Slow','value':'slow'}]}]},{'k':'checkLinkedinProfileIsContact','d':'Check LinkedIn Profile Is Contact','ed':'Check if a LinkedIn profile (url format accepted: https://linkedin.com/in/USERNAME) is a first degree contact','g':'communication','p':[{'n':'profile','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas'}]},{'k':'getLinkedinProfiles','d':'Get LinkedIn Profiles','ed':'Extract results from a LinkedIn search (url format accepted: https://linkedin.com/search/results/people/)','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/search/results/people/?facetGeoRegion=%5B%22es%3A5064%22%5D&facetNetwork=%5B%22S%22%2C%22O%22%5D&keywords=cmo%20barcelona'},{'n':'list','r':false,'t':'string','p':'my-list'},{'n':'mode','r':false,'t':'options','p':'fast','o':[{'name':'Fast','value':'fast'},{'name':'Normal','value':'normal'}]},{'n':'amount','r':false,'t':'string','p':'10'}]},{'k':'getLinkedinProfilesByCompany','d':'Get LinkedIn Company Employees','ed':'Extract results from a LinkedIn search (url format accepted: https://linkedin.com/search/results/people/)','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/company/ibm/'},{'n':'list','r':false,'t':'string','p':'employees'}]},{'k':'getLinkedinProfilesByContent','d':'Get LinkedIn Profiles By Content','ed':'Extract fastly last profiles that have published content on LinkedIn by specific keywords','g':'communication','p':[{'n':'keywords','r':false,'t':'string','p':'Growth'},{'n':'list','r':false,'t':'string','p':'content'}]},{'k':'getLinkedinPublicProfileBySalesProfile','d':'Get Public Profile By Sales Profile','ed':'Converts a Sales proofile to a LinkedIn public profile','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/sales/people/AAEAAA--ETMBUWoPI56yWsffNb3dk4FTn5nYJ20,NAME_SEARCH,Ik98/'}]},{'k':'sendLinkedinVisit','d':'Send LinkedIn Profile Visit','ed':'Visits a profile to show interest and get profile views in return from contact, increasing your LinkedIn network','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.linkedin.com/in/miquelcolomersalas/'}]},{'k':'checkListContains','d':'Check List Contains','ed':'Check if the list contains a specific item','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'text','r':true,'t':'string','p':'2'}]},{'k':'checkListEnds','d':'Check List Ends With','ed':'Check if the list ends with a specific element','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'text','r':true,'t':'string','p':'3'}]},{'k':'checkListLengthBetw','d':'Check Length List Between','ed':'Check if the length of a list is between two quantities','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length1','r':true,'t':'number','p':'3'},{'n':'length2','r':true,'t':'number','p':'4'}]},{'k':'checkListLengthEq','d':'Check Length List Equal','ed':'Checks if the length of a list equals a specified quantity','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkListLengthGe','d':'Check Length List Greater Or Equal','ed':'Check if the length of a list is greater than or equal to a certain amount','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkListLengthGt','d':'Check Length List Greater','ed':'Check if the length of a list is greater than a certain amount','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'2'}]},{'k':'checkListLengthLe','d':'Check Length List Lower Or Equal','ed':'Check if the length of a list is less than or equal to a certain amount','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkListLengthLt','d':'Check Length List Lower','ed':'','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkListMax','d':'Check Greater Element','ed':'Checks if the largest item in a list matches the provided item','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,4,8,3,4,5'},{'n':'number','r':true,'t':'string','p':'8'}]},{'k':'getListMax','d':'Get Greater Element','ed':'Returns the largest item in a list','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'checkListMin','d':'Check Lower Element','ed':'Checks if the smallest element in a list matches the provided element','g':'text','p':[{'n':'list','r':true,'t':'string','p':'5,6,1,3,7'},{'n':'number','r':true,'t':'string','p':'1'}]},{'k':'getListMin','d':'Get Lower Element','ed':'Returns the smallest item in a list','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'getListSort','d':'Get Sorted List','ed':'Returns an ascending sorted list','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'checkListSorted','d':'Check List Is Sorted','ed':'Check if a list is sorted in ascending order','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3,4,5'}]},{'k':'checkListStarts','d':'Check List Starts With','ed':'Check if the list starts with a specific element','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'text','r':true,'t':'string','p':'1'}]},{'k':'checkListUnique','d':'Check Unique Es List','ed':'Check if a list consists of unique elements','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3,4,5'}]},{'k':'getListUnique','d':'Get Unique List','ed':'Returns a single list, with no repeating elements','g':'text','p':[{'n':'list','r':true,'t':'string','p':'4,1,2,3,4'}]},{'k':'checkListValid','d':'Check Valid List','ed':'Check if the supplied values ​​form a valid list of elements','g':'text','p':[{'n':'list','r':true,'t':'string','p':'1,2,3'},{'n':'separator','r':true,'t':'string','p':','}]},{'k':'getLocaleByIp','d':'Get Locale Data By IP','ed':'Discover locale data (currency, language) by ipv4 or ipv6 address.','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationByCoordinates','d':'Get Location By Coordinates','ed':'Discover the city name, zipcode, province or country by latitude and longitude','g':'geographic','p':[{'n':'coordinates','r':true,'t':'string','p':'41.619206,2.2920828'}]},{'k':'getLocationByIp','d':'Get Location By IP','ed':'Discover the city name, zipcode, province, country, latitude and longitude from an ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getReputationByIp','d':'Get Reputation By IP','ed':'Discover reputation by ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getTimeByIp','d':'Get Time Data By IP','ed':'Discover datetime data by ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationByName','d':'Get Location By Name','ed':'Discover location data by name','g':'geographic','p':[{'n':'name','r':true,'t':'string','p':'Cyberclick S.L.'}]},{'k':'getLocationByPhone','d':'Get Location By Landline Phone (ES)','ed':'Discover the city and the province from a landline phone number (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'848491812'}]},{'k':'getLocationByZipcode','d':'Get Location By Zipcode (ES)','ed':'Discover the city and the province from a zipcode number (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getLocationExtendedByIp','d':'Get Extended Location By IP','ed':'Discover geographical, company, timezone and reputation data by IPv4 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationGeocodedByIp','d':'Get Geocoded Location By IP','ed':'Discover the city name, zipcode, province, country, latitude and longitude from an ipv4 or ipv6 address and geocodes it','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getLocationListByName','d':'Get Locations By Name','ed':'Get most relevants locations by name (Google Maps typical search)','g':'geographic','p':[{'n':'name','r':true,'t':'string','p':'Pintores Granollers'}]},{'k':'getLocationListByParams','d':'Get Locations By Parameters','ed':'Get most relevants locations by name, category, location and radius','g':'geographic','p':[{'n':'name','r':false,'t':'string','p':'Saba'},{'n':'category','r':false,'t':'options','p':'parking','o':[{'name':'Accounting','value':'accounting'},{'name':'Airport','value':'airport'},{'name':'Amusement_park','value':'amusement_park'},{'name':'Aquarium','value':'aquarium'},{'name':'Art_gallery','value':'art_gallery'},{'name':'Atm','value':'atm'},{'name':'Bakery','value':'bakery'},{'name':'Bank','value':'bank'},{'name':'Bar','value':'bar'},{'name':'Beauty_salon','value':'beauty_salon'},{'name':'Bicycle_store','value':'bicycle_store'},{'name':'Book_store','value':'book_store'},{'name':'Bowling_alley','value':'bowling_alley'},{'name':'Bus_station','value':'bus_station'},{'name':'Cafe','value':'cafe'},{'name':'Campground','value':'campground'},{'name':'Car_dealer','value':'car_dealer'},{'name':'Car_rental','value':'car_rental'},{'name':'Car_repair','value':'car_repair'},{'name':'Car_wash','value':'car_wash'},{'name':'Casino','value':'casino'},{'name':'Cemetery','value':'cemetery'},{'name':'Church','value':'church'},{'name':'City_hall','value':'city_hall'},{'name':'Clothing_store','value':'clothing_store'},{'name':'Convenience_store','value':'convenience_store'},{'name':'Courthouse','value':'courthouse'},{'name':'Dentist','value':'dentist'},{'name':'Department_store','value':'department_store'},{'name':'Doctor','value':'doctor'},{'name':'Electrician','value':'electrician'},{'name':'Electronics_store','value':'electronics_store'},{'name':'Embassy','value':'embassy'},{'name':'Establishment','value':'establishment'},{'name':'Finance','value':'finance'},{'name':'Fire_station','value':'fire_station'},{'name':'Florist','value':'florist'},{'name':'Food','value':'food'},{'name':'Funeral_home','value':'funeral_home'},{'name':'Furniture_store','value':'furniture_store'},{'name':'Gas_station','value':'gas_station'},{'name':'General_contractor','value':'general_contractor'},{'name':'Grocery_or_supermarket','value':'grocery_or_supermarket'},{'name':'Gym','value':'gym'},{'name':'Hair_care','value':'hair_care'},{'name':'Hardware_store','value':'hardware_store'},{'name':'Health','value':'health'},{'name':'Hindu_temple','value':'hindu_temple'},{'name':'Home_goods_store','value':'home_goods_store'},{'name':'Hospital','value':'hospital'},{'name':'Insurance_agency','value':'insurance_agency'},{'name':'Jewelry_store','value':'jewelry_store'},{'name':'Laundry','value':'laundry'},{'name':'Lawyer','value':'lawyer'},{'name':'Library','value':'library'},{'name':'Liquor_store','value':'liquor_store'},{'name':'Local_government_office','value':'local_government_office'},{'name':'Locksmith','value':'locksmith'},{'name':'Lodging','value':'lodging'},{'name':'Meal_delivery','value':'meal_delivery'},{'name':'Meal_takeaway','value':'meal_takeaway'},{'name':'Mosque','value':'mosque'},{'name':'Movie_rental','value':'movie_rental'},{'name':'Movie_theater','value':'movie_theater'},{'name':'Moving_location','value':'moving_location'},{'name':'Museum','value':'museum'},{'name':'Night_club','value':'night_club'},{'name':'Painter','value':'painter'},{'name':'Park','value':'park'},{'name':'Parking','value':'parking'},{'name':'Pet_store','value':'pet_store'},{'name':'Pharmacy','value':'pharmacy'},{'name':'Physiotherapist','value':'physiotherapist'},{'name':'Place_of_worship','value':'place_of_worship'},{'name':'Plumber','value':'plumber'},{'name':'Police','value':'police'},{'name':'Post_office','value':'post_office'},{'name':'Real_estate_agency','value':'real_estate_agency'},{'name':'Restaurant','value':'restaurant'},{'name':'Roofing_contractor','value':'roofing_contractor'},{'name':'Rv_park','value':'rv_park'},{'name':'School','value':'school'},{'name':'Shoe_store','value':'shoe_store'},{'name':'Shopping_mall','value':'shopping_mall'},{'name':'Spa','value':'spa'},{'name':'Stadium','value':'stadium'},{'name':'Storage','value':'storage'},{'name':'Store','value':'store'},{'name':'Subway_station','value':'subway_station'},{'name':'Synagogue','value':'synagogue'},{'name':'Taxi_stand','value':'taxi_stand'},{'name':'Train_station','value':'train_station'},{'name':'Transit_station','value':'transit_station'},{'name':'Travel_agency','value':'travel_agency'},{'name':'University','value':'university'},{'name':'Veterinary_care','value':'veterinary_care'},{'name':'Zoo','value':'zoo'}]},{'n':'location','r':true,'t':'string','p':'41.3851,2.1734'},{'n':'radius','r':false,'t':'string','p':'250'}]},{'k':'checkMobileAlive','d':'Check Mobile Is Alive','ed':'Discover if a mobile number is switched on to call it later, with worldwide coverage.
Some carriers don\'t return if mobile is alive (like Vodafone)','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileCountryCode','d':'Get Country ISO Code By Mobile','ed':'Allow to get country code (two chars) of a mobile phone number with international format','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileCountryPrefix','d':'Get Phone Prefix By Country ISO Code','ed':'Allow to get country prefix number by country code (2 characters)','g':'communication','p':[{'n':'country','r':true,'t':'string','p':'ES'}]},{'k':'checkMobileExist','d':'Check Mobile Exists','ed':'Discover if mobile phone number exists in network operator, with worldwide coverage.
Get advanced mobile KPIs with \'Mobile lookup\' tool.','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileFormat','d':'Check Mobile Has Valid Format','ed':'Discover if mobile phone number has a valid format, with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34623123213'},{'n':'country','r':true,'t':'string','p':'ES'}]},{'k':'checkMobileFormatEs','d':'Check Mobile Has Valid Format (ES)','ed':'Discover if mobile phone number has a valid format (only Spain)','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'623123213'}]},{'k':'getMobileFormatted','d':'Get Formatted Mobile','ed':'Format international mobile number by country ISO code (2 letters).','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'},{'n':'country','r':false,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'getMobileHlrLookup','d':'Get HLR Mobile Lookup','ed':'Discover if mobile exist via real time [HLR](https://en.wikipedia.org/wiki/Home_location_register) query','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileImei','d':'Check Imei Is Valid','ed':'Discover if Imei number has a valid format','g':'communication','p':[{'n':'imei','r':true,'t':'string','p':'490154203237518'}]},{'k':'getMobileLookup','d':'Get Mobile Lookup','ed':'Discover if mobile exist via real time [HLR](https://en.wikipedia.org/wiki/Home_location_register) query, as well as portability and roaming data','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileMnpLookup','d':'Get Mobile Portability Lookup','ed':'Get existence, portability and roaming of a mobile phone, via [MNP](https://en.wikipedia.org/wiki/Mobile_number_portability) query','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileNormalized','d':'Get Normalized Mobile','ed':'Allow to normalize a mobile phone, removing non allowed characters','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34 62318 2 770'},{'n':'country','r':false,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'getMobileOrPhoneLookupEs','d':'Get Mobile/Landline Lookup (Spain)','ed':'Discover if mobile or landline prefix exists on Spain.','g':'communication','p':[{'n':'number','r':true,'t':'string','p':'34605281220'}]},{'k':'getMobileOrPhoneMnpEs','d':'Get Landline/Mobile Portability Lookup (ES)','ed':'Get portability data about a landline or mobile number, only for Spain','g':'communication','p':[{'n':'number','r':true,'t':'string','p':'605281220'}]},{'k':'checkMobileSms','d':'Check Mobile Supports Sms','ed':'Discover if a mobile number can receive sms, with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileValidPrefix','d':'Check Mobile Has Valid Prefix','ed':'Discover if mobile phone number has a valid prefix, with worldwide coverage','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'checkMobileValidPrefixEs','d':'Check Mobile Has Valid Prefix (ES)','ed':'Discover if spanish mobile phone number has a valid prefix','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'605281220'}]},{'k':'getNameByPrefix','d':'Get Name By Prefix (ES)','ed':'Get first personal name matching by prefix and gender from INE data source (only Spain)','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Marce'},{'n':'gender','r':false,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]}]},{'k':'checkNameExist','d':'Check Name Exists (ES)','ed':'Check if a personal name exists in INE data source (only Spain)','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Juan'}]},{'k':'getNameListByPrefix','d':'Get Names By Prefix','ed':'Get multiple personal names by prefix','g':'geographic','p':[{'n':'name','r':true,'t':'string','p':'Marce'},{'n':'gender','r':false,'t':'options','p':'male','o':[{'name':'Female','value':'female'},{'name':'Male','value':'male'}]}]},{'k':'getNameNormalized','d':'Get Normalized Name','ed':'Normalize name removing non allowed characters','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'M4rc'}]},{'k':'checkNameValid','d':'Check Name Has Valid Format','ed':'Check if name contains accepted characters','g':'personal','p':[{'n':'name','r':true,'t':'string','p':'Gonzalez'}]},{'k':'getUrlAnalysis','d':'Get URL Analysis','ed':'Analyze URL\'s health status about SSL, broken links, conflictive HTTP links with SSL, and more.','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'}]},{'k':'getNetAton','d':'Get Number By IP','ed':'Convert an IP address to numeric notation','g':'internet','p':[{'n':'ip','r':true,'t':'string','p':'62.12.22.11'}]},{'k':'getNetByIp','d':'Get Network By IP','ed':'Discover network data by ipv4 or ipv6 address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'checkUrlContains','d':'Check URL Contains','ed':'Check if an URL contains string or regular expression (case insensitive)','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://uproc.io'},{'n':'regex','r':true,'t':'string','p':'uProc'}]},{'k':'getUrlContents','d':'Get Contents From URL','ed':'Get text data from web, pdf or image (png, jpg, gif), allowing to filter some elements by regular expressions or field names (email, phone, zipcode).

Learn about regular expressions on [Wikipedia](https://en.wikipedia.org/wiki/Regular_expression)','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://docs.uproc.io/pdf/resumen_del_servicio_EN.pdf'},{'n':'selector','r':false,'t':'string','p':'emails'}]},{'k':'getUrlContentsParsed','d':'Get Parsed Contents From URL','ed':'Obtains the content of a web in a structured way in JSON format to be able to save it wherever you want','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.google.com/search?q=killia+Technologies'}]},{'k':'getUrlDecode','d':'Get Decoded URL','ed':'Decode URL to recover original','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https%3A%2F%2Fgoogle.es'}]},{'k':'getUrlEncode','d':'Get Encoded URL','ed':'Encode URL to avoid problems','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'http://google.com'}]},{'k':'checkUrlExist','d':'Check URL Exists','ed':'Check if an URL exists','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'http://www.google.com'}]},{'k':'getNetFixip','d':'Get Fixed IP','ed':'Fix an IP address to the right format','g':'internet','p':[{'n':'number','r':true,'t':'string','p':'212169160147'}]},{'k':'checkNetHostAlive','d':'Check Host Is Up','ed':'Discover if a computer is switched on','g':'internet','p':[{'n':'host','r':true,'t':'string','p':'www.google.es'}]},{'k':'checkStringIp','d':'Check IP Has Valid Format','ed':'Check if IPv4 or IPv6 address has a valid format','g':'text','p':[{'n':'ip','r':true,'t':'string','p':'23.45.57.123'}]},{'k':'checkStringIp4','d':'Check IPv4 Has Valid Format','ed':'Check if IPv4 address has a valid format','g':'text','p':[{'n':'ip','r':true,'t':'string','p':'127.0.0.1'}]},{'k':'checkStringIp6','d':'Check IPv6 Has Valid Format','ed':'Check if IPv6 address has a valid format','g':'text','p':[{'n':'ip','r':true,'t':'string','p':'2a01:c50e:3544:bd00:4df0:7609:251a:f6d0'}]},{'k':'getUrlListContentsParsed','d':'Get Parsed Contents From Results URL','ed':'Obtains a list with multiple results from a website in a structured way in JSON format to be able to save it wherever you want','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://www.google.com/search?q=killia+Technologies'}]},{'k':'getNetNtoa','d':'Get IP By Number','ed':'Convert a number to an IP address','g':'internet','p':[{'n':'number','r':true,'t':'string','p':'1501706957'}]},{'k':'getUrlParsed','d':'Get Parsed URL','ed':'Decode URL into multiple fields','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://docs.uproc.io/pdf/resumen_del_servicio_EN.pdf'}]},{'k':'getNetScan','d':'Get Opened Ports In Host','ed':'Scan a host and returns most common open ports: 21, 22, 23, 25, 53, 80, 110, 143, 443, 3306, 27017','g':'internet','p':[{'n':'host','r':true,'t':'string','p':'google.es'}]},{'k':'checkNetServiceUp','d':'Check Service Is Up','ed':'Discover if a service in a port is available','g':'internet','p':[{'n':'host','r':true,'t':'string','p':'www.google.com'},{'n':'port','r':true,'t':'string','p':'80'}]},{'k':'getUrlTables','d':'Get Table From URL','ed':'Get data from existing table in HTML page (by table number) or in a PDF file (by table column number) in CSV format (columns delimited by ;)','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2'},{'n':'table','r':false,'t':'string','p':'3'}]},{'k':'checkUrlValid','d':'Check URL Is Valid','ed':'Check that an URL has a valid format','g':'internet','p':[{'n':'url','r':true,'t':'string','p':'http://www.google.com'}]},{'k':'checkNumberBetw','d':'Check Number Is Between','ed':'Check if number is between two values','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'21'},{'n':'number3','r':true,'t':'string','p':'24'}]},{'k':'checkNumberDecimal','d':'Check Decimal Number Is Valid','ed':'Check if value is a decimal number','p':[{'n':'number','r':true,'t':'string','p':'0.23'}]},{'k':'checkNumberEq','d':'Check Number Is Equal','ed':'Check if number is equal to another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberEven','d':'Check Even Number Is Valid','ed':'Check if number is even','p':[{'n':'number','r':true,'t':'string','p':'2'}]},{'k':'checkNumberGe','d':'Check Number Is Greater Or Equal','ed':'Check if number is greater or equal than another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberGt','d':'Check Number Is Greater','ed':'Check if number is greater than another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'22'}]},{'k':'checkNumberLe','d':'Check Number Is Lower Or Equal','ed':'Check if number is lower or equal than another','p':[{'n':'number1','r':true,'t':'string','p':'23'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberLt','d':'Check Number Is Lower','ed':'Check if number is lower than another','p':[{'n':'number1','r':true,'t':'string','p':'22'},{'n':'number2','r':true,'t':'string','p':'23'}]},{'k':'checkNumberLuhn','d':'Check Luhn Number Is Valid','ed':'Check if it a valid Luhn number','g':'security','p':[{'n':'luhn','r':true,'t':'string','p':'79927398713'}]},{'k':'checkNumberMod','d':'Check Modulus Is Equals','ed':'Check if modulus between two numbers is equal to a value','p':[{'n':'number','r':true,'t':'string','p':'10'},{'n':'mod','r':true,'t':'string','p':'2'},{'n':'rest','r':true,'t':'string','p':'0'}]},{'k':'checkNumberNatural','d':'Check Natural Number Is Valid','ed':'Check if value is a natural number','p':[{'n':'number','r':true,'t':'string','p':'0'}]},{'k':'checkStringNumeric','d':'Check Numeric String Is Valid','ed':'Check if string length contains only numbers','g':'text','p':[{'n':'text','r':true,'t':'string','p':'123'}]},{'k':'checkNumberOdd','d':'Check Odd Number Is Valid','ed':'Check if number is odd','p':[{'n':'number','r':true,'t':'string','p':'3'}]},{'k':'checkNumberPrime','d':'Check Prime Number Is Valid','ed':'Check if number is prime','p':[{'n':'number','r':true,'t':'string','p':'11'}]},{'k':'checkPasswordStrong','d':'Check Password Is Strong','ed':'Check is password is sure and contains a lowercase, uppercase, numbers, special characters and have a minimum length of four characters','g':'security','p':[{'n':'password','r':true,'t':'string','p':'1agdA*$#'}]},{'k':'getPersonByEmail','d':'Get Person By Email','ed':'Get personal data by email','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getPersonByFirstnameLastnameCompanyLocation','d':'Get Person By Firstname, Lastname, Company And Location','ed':'Get personal data by firstname, lastname, company and location','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'Miquel'},{'n':'lastname','r':true,'t':'string','p':'Colomer'},{'n':'company','r':false,'t':'string','p':'uProc'},{'n':'location','r':false,'t':'string','p':''}]},{'k':'getPersonByMobile','d':'Get Person By Mobile','ed':'Get personal data by mobile','g':'personal','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'sendPersonEmailToList','d':'Send Email\'s Contact To List','ed':'Add a contact email to a person list','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'},{'n':'list','r':false,'t':'string','p':'my-list'}]},{'k':'getPersonExtendedByEmail','d':'Get Person (Extended) By Email','ed':'Get prospect\'s contact data and company\'s location and social data by email','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getPersonExtendedByEmailAndCompany','d':'Get Person (Extended) By Email And Company','ed':'Get contact, location and social data by email and company name and location','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'},{'n':'company','r':false,'t':'string','p':'uProc'}]},{'k':'getPersonFakedData','d':'Get Random Person Data','ed':'Generates random fake data','g':'personal','p':[{'n':'locality','r':true,'t':'options','p':'English','o':[{'name':'Australia (English)','value':'Australia (English)'},{'name':'Australia Ocker (English)','value':'Australia Ocker (English)'},{'name':'Azerbaijani','value':'Azerbaijani'},{'name':'Bork (English)','value':'Bork (English)'},{'name':'Canada (English)','value':'Canada (English)'},{'name':'Canada (French)','value':'Canada (French)'},{'name':'Chinese','value':'Chinese'},{'name':'Chinese (Taiwan)','value':'Chinese (Taiwan)'},{'name':'Czech','value':'Czech'},{'name':'Dutch','value':'Dutch'},{'name':'English','value':'English'},{'name':'Farsi','value':'Farsi'},{'name':'French','value':'French'},{'name':'Georgian','value':'Georgian'},{'name':'German','value':'German'},{'name':'German (Austria)','value':'German (Austria)'},{'name':'German (Switzerland)','value':'German (Switzerland)'},{'name':'Great Britain (English)','value':'Great Britain (English)'},{'name':'India (English)','value':'India (English)'},{'name':'Indonesia','value':'Indonesia'},{'name':'Ireland (English)','value':'Ireland (English)'},{'name':'Italian','value':'Italian'},{'name':'Japanese','value':'Japanese'},{'name':'Korean','value':'Korean'},{'name':'Nepalese','value':'Nepalese'},{'name':'Norwegian','value':'Norwegian'},{'name':'Polish','value':'Polish'},{'name':'Portuguese (Brazil)','value':'Portuguese (Brazil)'},{'name':'Russian','value':'Russian'},{'name':'Slovakian','value':'Slovakian'},{'name':'Spanish','value':'Spanish'},{'name':'Spanish Mexico','value':'Spanish Mexico'},{'name':'Swedish','value':'Swedish'},{'name':'Turkish','value':'Turkish'},{'name':'Ukrainian','value':'Ukrainian'},{'name':'United States (English)','value':'United States (English)'},{'name':'Vietnamese','value':'Vietnamese'}]}]},{'k':'getProfileByEmployeeData','d':'Get LinkedIn URI By First, Last And Company','ed':'Get LinkedIn employee profile URI by firstname, lastname and company without manual search on Google or LinkedIn.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'Miquel'},{'n':'lastname','r':true,'t':'string','p':'Colomer'},{'n':'company','r':true,'t':'string','p':'uProc'}]},{'k':'getSurnameByPrefix','d':'Get Surname By Prefix (ES)','ed':'Get first personal surname matching by prefix from INE data source (only Spain)','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Col'}]},{'k':'checkSurnameExist','d':'Check Surname Is Valid (ES)','ed':'Check if a personal surname appears in INE data source (only Spain)','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Gonzalez'}]},{'k':'getProfileLinkedinByEmail','d':'Get LinkedIn URI By Email','ed':'Get LinkedIn employee profile URI by business email.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'getSurnameListByPrefix','d':'Get Surnames By Prefix (ES)','ed':'Get personal surnames matching by prefix from INE data source (only Spain)','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Co'}]},{'k':'getSurnameNormalized','d':'Get Normalized Surname','ed':'Normalize surname','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'C0lomer'}]},{'k':'getProfileTwitterByEmployeeData','d':'Get Twitter URI By First, Last And Company','ed':'Get Twitter profile by firstname, lastname and company without manual search on Google or Twitter.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'Carlos'},{'n':'lastname','r':true,'t':'string','p':'Blanco'},{'n':'company','r':true,'t':'string','p':'Encomenda'}]},{'k':'checkSurnameValid','d':'Check Surname Has Valid Format','ed':'Check if surname contains accepted characters','g':'personal','p':[{'n':'surname','r':true,'t':'string','p':'Gonzalez'}]},{'k':'getProfileXingByEmployeeData','d':'Get Xing URI By First, Last And Company','ed':'Get Xing profile by firstname, lastname and company without manual search on Google or Xing.

This tool uses search engines (Bing and Google) through proxies','g':'personal','p':[{'n':'firstname','r':true,'t':'string','p':'David'},{'n':'lastname','r':true,'t':'string','p':'Tomás'},{'n':'company','r':true,'t':'string','p':'Cyberclick'}]},{'k':'getPhoneFixed','d':'Get Fixed Phone','ed':'Fix the international prefix of a phone based on the ISO code of a country','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'+1605281220'},{'n':'country','r':true,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'checkPhoneFormat','d':'Check Valid Phone Format By Country','ed':'Allow to discover if landline number has a good international format, depending on country ','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'},{'n':'country','r':true,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'checkPhoneFormatEs','d':'Check Valid Phone Format (ES)','ed':'Discover if landline phone number is valid, with Spain coverage','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'932187670'}]},{'k':'getPhoneNormalized','d':'Get Cleaned Phone','ed':'Clean a phone removing non allowed characters','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'3493218 767o'},{'n':'country','r':false,'t':'options','p':'ES','o':[{'name':'AD','value':'AD'},{'name':'AE','value':'AE'},{'name':'AF','value':'AF'},{'name':'AG','value':'AG'},{'name':'AI','value':'AI'},{'name':'AL','value':'AL'},{'name':'AM','value':'AM'},{'name':'AO','value':'AO'},{'name':'AQ','value':'AQ'},{'name':'AR','value':'AR'},{'name':'AS','value':'AS'},{'name':'AT','value':'AT'},{'name':'AU','value':'AU'},{'name':'AW','value':'AW'},{'name':'AX','value':'AX'},{'name':'AZ','value':'AZ'},{'name':'BA','value':'BA'},{'name':'BB','value':'BB'},{'name':'BD','value':'BD'},{'name':'BE','value':'BE'},{'name':'BF','value':'BF'},{'name':'BG','value':'BG'},{'name':'BH','value':'BH'},{'name':'BI','value':'BI'},{'name':'BJ','value':'BJ'},{'name':'BL','value':'BL'},{'name':'BM','value':'BM'},{'name':'BN','value':'BN'},{'name':'BO','value':'BO'},{'name':'BQ','value':'BQ'},{'name':'BR','value':'BR'},{'name':'BS','value':'BS'},{'name':'BT','value':'BT'},{'name':'BV','value':'BV'},{'name':'BW','value':'BW'},{'name':'BY','value':'BY'},{'name':'BZ','value':'BZ'},{'name':'CA','value':'CA'},{'name':'CC','value':'CC'},{'name':'CD','value':'CD'},{'name':'CF','value':'CF'},{'name':'CG','value':'CG'},{'name':'CH','value':'CH'},{'name':'CI','value':'CI'},{'name':'CK','value':'CK'},{'name':'CL','value':'CL'},{'name':'CM','value':'CM'},{'name':'CN','value':'CN'},{'name':'CO','value':'CO'},{'name':'CR','value':'CR'},{'name':'CU','value':'CU'},{'name':'CV','value':'CV'},{'name':'CW','value':'CW'},{'name':'CX','value':'CX'},{'name':'CY','value':'CY'},{'name':'CZ','value':'CZ'},{'name':'DE','value':'DE'},{'name':'DJ','value':'DJ'},{'name':'DK','value':'DK'},{'name':'DM','value':'DM'},{'name':'DO','value':'DO'},{'name':'DZ','value':'DZ'},{'name':'EC','value':'EC'},{'name':'EE','value':'EE'},{'name':'EG','value':'EG'},{'name':'EH','value':'EH'},{'name':'ER','value':'ER'},{'name':'ES','value':'ES'},{'name':'ET','value':'ET'},{'name':'FI','value':'FI'},{'name':'FJ','value':'FJ'},{'name':'FK','value':'FK'},{'name':'FM','value':'FM'},{'name':'FO','value':'FO'},{'name':'FR','value':'FR'},{'name':'GA','value':'GA'},{'name':'GB','value':'GB'},{'name':'GD','value':'GD'},{'name':'GE','value':'GE'},{'name':'GF','value':'GF'},{'name':'GG','value':'GG'},{'name':'GH','value':'GH'},{'name':'GI','value':'GI'},{'name':'GL','value':'GL'},{'name':'GM','value':'GM'},{'name':'GN','value':'GN'},{'name':'GP','value':'GP'},{'name':'GQ','value':'GQ'},{'name':'GR','value':'GR'},{'name':'GS','value':'GS'},{'name':'GT','value':'GT'},{'name':'GU','value':'GU'},{'name':'GW','value':'GW'},{'name':'GY','value':'GY'},{'name':'HK','value':'HK'},{'name':'HM','value':'HM'},{'name':'HN','value':'HN'},{'name':'HR','value':'HR'},{'name':'HT','value':'HT'},{'name':'HU','value':'HU'},{'name':'ID','value':'ID'},{'name':'IE','value':'IE'},{'name':'IL','value':'IL'},{'name':'IM','value':'IM'},{'name':'IN','value':'IN'},{'name':'IO','value':'IO'},{'name':'IQ','value':'IQ'},{'name':'IR','value':'IR'},{'name':'IS','value':'IS'},{'name':'IT','value':'IT'},{'name':'JE','value':'JE'},{'name':'JM','value':'JM'},{'name':'JO','value':'JO'},{'name':'JP','value':'JP'},{'name':'KE','value':'KE'},{'name':'KG','value':'KG'},{'name':'KH','value':'KH'},{'name':'KI','value':'KI'},{'name':'KM','value':'KM'},{'name':'KN','value':'KN'},{'name':'KP','value':'KP'},{'name':'KR','value':'KR'},{'name':'KW','value':'KW'},{'name':'KY','value':'KY'},{'name':'KZ','value':'KZ'},{'name':'LA','value':'LA'},{'name':'LB','value':'LB'},{'name':'LC','value':'LC'},{'name':'LI','value':'LI'},{'name':'LK','value':'LK'},{'name':'LR','value':'LR'},{'name':'LS','value':'LS'},{'name':'LT','value':'LT'},{'name':'LU','value':'LU'},{'name':'LV','value':'LV'},{'name':'LY','value':'LY'},{'name':'MA','value':'MA'},{'name':'MC','value':'MC'},{'name':'MD','value':'MD'},{'name':'ME','value':'ME'},{'name':'MF','value':'MF'},{'name':'MG','value':'MG'},{'name':'MH','value':'MH'},{'name':'MK','value':'MK'},{'name':'ML','value':'ML'},{'name':'MM','value':'MM'},{'name':'MN','value':'MN'},{'name':'MO','value':'MO'},{'name':'MP','value':'MP'},{'name':'MQ','value':'MQ'},{'name':'MR','value':'MR'},{'name':'MS','value':'MS'},{'name':'MT','value':'MT'},{'name':'MU','value':'MU'},{'name':'MV','value':'MV'},{'name':'MW','value':'MW'},{'name':'MX','value':'MX'},{'name':'MY','value':'MY'},{'name':'MZ','value':'MZ'},{'name':'NA','value':'NA'},{'name':'NC','value':'NC'},{'name':'NE','value':'NE'},{'name':'NF','value':'NF'},{'name':'NG','value':'NG'},{'name':'NI','value':'NI'},{'name':'NL','value':'NL'},{'name':'NO','value':'NO'},{'name':'NP','value':'NP'},{'name':'NR','value':'NR'},{'name':'NU','value':'NU'},{'name':'NZ','value':'NZ'},{'name':'OM','value':'OM'},{'name':'PA','value':'PA'},{'name':'PE','value':'PE'},{'name':'PF','value':'PF'},{'name':'PG','value':'PG'},{'name':'PH','value':'PH'},{'name':'PK','value':'PK'},{'name':'PL','value':'PL'},{'name':'PM','value':'PM'},{'name':'PN','value':'PN'},{'name':'PR','value':'PR'},{'name':'PS','value':'PS'},{'name':'PT','value':'PT'},{'name':'PW','value':'PW'},{'name':'PY','value':'PY'},{'name':'QA','value':'QA'},{'name':'RE','value':'RE'},{'name':'RO','value':'RO'},{'name':'RS','value':'RS'},{'name':'RU','value':'RU'},{'name':'RW','value':'RW'},{'name':'SA','value':'SA'},{'name':'SB','value':'SB'},{'name':'SC','value':'SC'},{'name':'SD','value':'SD'},{'name':'SE','value':'SE'},{'name':'SG','value':'SG'},{'name':'SH','value':'SH'},{'name':'SI','value':'SI'},{'name':'SJ','value':'SJ'},{'name':'SK','value':'SK'},{'name':'SL','value':'SL'},{'name':'SM','value':'SM'},{'name':'SN','value':'SN'},{'name':'SO','value':'SO'},{'name':'SR','value':'SR'},{'name':'SS','value':'SS'},{'name':'ST','value':'ST'},{'name':'SV','value':'SV'},{'name':'SX','value':'SX'},{'name':'SY','value':'SY'},{'name':'SZ','value':'SZ'},{'name':'TC','value':'TC'},{'name':'TD','value':'TD'},{'name':'TF','value':'TF'},{'name':'TG','value':'TG'},{'name':'TH','value':'TH'},{'name':'TJ','value':'TJ'},{'name':'TK','value':'TK'},{'name':'TL','value':'TL'},{'name':'TM','value':'TM'},{'name':'TN','value':'TN'},{'name':'TO','value':'TO'},{'name':'TR','value':'TR'},{'name':'TT','value':'TT'},{'name':'TV','value':'TV'},{'name':'TW','value':'TW'},{'name':'TZ','value':'TZ'},{'name':'UA','value':'UA'},{'name':'UG','value':'UG'},{'name':'UM','value':'UM'},{'name':'US','value':'US'},{'name':'UY','value':'UY'},{'name':'UZ','value':'UZ'},{'name':'VA','value':'VA'},{'name':'VC','value':'VC'},{'name':'VE','value':'VE'},{'name':'VG','value':'VG'},{'name':'VI','value':'VI'},{'name':'VN','value':'VN'},{'name':'VU','value':'VU'},{'name':'WF','value':'WF'},{'name':'WS','value':'WS'},{'name':'YE','value':'YE'},{'name':'YT','value':'YT'},{'name':'ZA','value':'ZA'},{'name':'ZM','value':'ZM'},{'name':'ZW','value':'ZW'}]}]},{'k':'checkPhoneOrMobileValid','d':'Check Phone Or Mobile Valid','ed':'Discover if landline or mobile number has a valid prefix','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'},{'n':'country','r':false,'t':'string','p':'ES'}]},{'k':'getPhoneParsed','d':'Get Parsed And Validated Phone','ed':'Parse phone number in multiple fields and verify format and prefix validity (phone existence is not checked)','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'}]},{'k':'checkPhoneValidPrefix','d':'Check Valid Phone Prefix','ed':'Discover if a landline phone number prefix exists, with worldwide coverage','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'34932187670'}]},{'k':'getProvinceByIp','d':'Get Province By IP','ed':'Discover the province name from an IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getProvinceByName','d':'Get Province By Name (ES)','ed':'You can get the first province by a name prefix (only Spain)','g':'geographic','p':[{'n':'province','r':true,'t':'string','p':'B'}]},{'k':'getProvinceByPhone','d':'Get Province By Phone (ES)','ed':'Discover the province name from a landline phone number (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'932'}]},{'k':'getProvinceByZipcode','d':'Get Province By Zipcode (ES)','ed':'Discover the province name from a zipcode number (only Spain)','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08'}]},{'k':'getProvinceListByName','d':'Get Provinces By Name (ES)','ed':'You can get a province list by a name prefix (only Spain)','g':'geographic','p':[{'n':'province','r':true,'t':'string','p':'B'}]},{'k':'getProvinceListByPhone','d':'Get Provinces By Phone (ES)','ed':'You can get a province list by a phone prefix (only Spain)','g':'geographic','p':[{'n':'phone','r':true,'t':'string','p':'932'}]},{'k':'getProvinceListByZipcode','d':'Get Provinces By Zipcode (ES)','ed':'You can get a province list by a zipcode prefix, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'0'}]},{'k':'getProvinceNormalized','d':'Get Normalized Province','ed':'Allow to normalize a province, removing non allowed characters','g':'geographic','p':[{'n':'province','r':true,'t':'string','p':'Barce lona'}]},{'k':'checkRobinsonEmailExist','d':'Check Email Is Robinson (ES)','ed':'Discover if an email exists in the Robinson list (only Spain)','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'manzaned@uvigo.es'}]},{'k':'checkRobinsonNifExist','d':'Check Nif Number Is Robinson (ES)','ed':'Discover if an nif exists in the Robinson list (only Spain)','g':'personal','p':[{'n':'nif','r':true,'t':'string','p':'00002206K'}]},{'k':'checkRobinsonPhoneExist','d':'Check Phone Is Robinson (ES)','ed':'Discover if a phone (landline or mobile) exists in a Robinson list (only Spain)','g':'communication','p':[{'n':'phone','r':true,'t':'string','p':'917156252'}]},{'k':'getSentimentByText','d':'Get Sentiment From A Text','ed':'It allows to analyze an english text with Emojis and detect sentiment','g':'text','p':[{'n':'text','r':true,'t':'string','p':'I am very happy'}]},{'k':'checkSocialDomainExist','d':'Check Domain Has Social Activity','ed':'Discover if a domain has social network presence','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getSocialDomainLookup','d':'Get Social Networks By Domain','ed':'Discover if a domain or a website has social activity and returns all social network profiles found','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'getSocialDomainParsed','d':'Get Social Networks Activity By Domain','ed':'Search all social networks by domain, parses all found urls and returns social networks kpis (if data available)','g':'communication','p':[{'n':'domain','r':true,'t':'string','p':'cyberclick.es'}]},{'k':'checkSocialEmailExist','d':'Check Email Has Social Activity','ed':'Discover if the email has social network presence','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'mcolomer@gmail.com'}]},{'k':'getSocialEmailLookup','d':'Get Social Networks By Email','ed':'Discover if an email has social activity, and get all social network profiles found','g':'communication','p':[{'n':'email','r':true,'t':'string','p':'miquel@uproc.io'}]},{'k':'checkSocialMobileExist','d':'Check Mobile Has Social Activity','ed':'Discover if the mobile phone has social network presence','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getSocialMobileLookup','d':'Get Social Networks By Mobile','ed':'Discover if an mobile phone has social activity, and get all social network profiles found','g':'communication','p':[{'n':'mobile','r':true,'t':'string','p':'34605281220'}]},{'k':'getSocialUriParsed','d':'Get Social Network Activity','ed':'This tools parses a social uri address and extracts any available indicators','g':'communication','p':[{'n':'url','r':true,'t':'string','p':'https://www.facebook.com/uprocdataquality'}]},{'k':'checkStringAlpha','d':'Check Alphabetic String Is Valid','ed':'Check if string length contains only letters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'asc'}]},{'k':'checkStringAlphanumeric','d':'Check Alphanumeric String Is Valid','ed':'Check if string length contains only numbers and letters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'aa11'}]},{'k':'getStringBase64','d':'Get BASE64 Value','ed':'Convert a string to a BASE64 encoded value','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'}]},{'k':'checkStringBlank','d':'Check String Is Empty','ed':'Check if string has no content','g':'text','p':[{'n':'text','r':true,'t':'string','p':''}]},{'k':'checkStringBoolean','d':'Check Boolean String Is Valid','ed':'Check if string is true or false','g':'text','p':[{'n':'text','r':true,'t':'string','p':'true'}]},{'k':'getStringByFormat','d':'Get Formatted String By Pattern','ed':'It allows to format a string using a format pattern','g':'text','p':[{'n':'text','r':true,'t':'string','p':'1122'},{'n':'format','r':true,'t':'string','p':'%09d'}]},{'k':'getStringByRegex','d':'Get Generated Text By Pattern','ed':'Generate a random string using a regular expression as a pattern','g':'text','p':[{'n':'regex','r':true,'t':'string','p':'[0-9]{,2}-[a-zA-Z]{2,3}-[A-Z]{2,5}-[0-9]{2}-[a-z]*'}]},{'k':'checkStringContains','d':'Check String Contains Char','ed':'Check if string contains a character','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'test'},{'n':'text2','r':true,'t':'string','p':'t'}]},{'k':'checkStringEnds','d':'Check String Ends With','ed':'Check if string ends with a character','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'test'},{'n':'text2','r':true,'t':'string','p':'t'}]},{'k':'getStringFieldName','d':'Get Field Type By Value','ed':'Get field name, analyzing field value provided.

Supported values: Email, Domain, Isbn, Ean, Upc, Dni, Nie, Cif, Date, Gender (male, female), Landline, Mobile phone, Zip code, Web address','g':'text','p':[{'n':'text','r':true,'t':'string','p':'myemail@mydomain.com'}]},{'k':'getStringJoin','d':'Get Merged Values By Text','ed':'Join a character or string to join two values','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'hi'},{'n':'text2','r':true,'t':'string','p':'good morning!'},{'n':'glue','r':true,'t':'string','p':','}]},{'k':'getStringLength','d':'Get String Length','ed':'Get length of a string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, Mike!'}]},{'k':'checkStringLengthBetw','d':'Check String Length Is Between','ed':'Check if string length is between two numbers','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length1','r':true,'t':'number','p':'3'},{'n':'length2','r':true,'t':'number','p':'5'}]},{'k':'checkStringLengthEq','d':'Check String Length Is Equal','ed':'Check if string length is equal to number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkStringLengthGe','d':'Check String Length Is Greater Or Equal','ed':'Check if string length is greater or equal than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkStringLengthGt','d':'Check String Length Is Greater','ed':'Check if string length is greater than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'3'}]},{'k':'checkStringLengthLe','d':'Check String Length Is Lower Or Equal','ed':'Check if string length is lower or equal than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'4'}]},{'k':'checkStringLengthLt','d':'Check String Length Is Lower','ed':'Check if string length is lower than number','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'length','r':true,'t':'number','p':'5'}]},{'k':'checkStringLowercase','d':'Check Text Is Lowercased','ed':'Check if string only contains lowercase characters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'aaa'}]},{'k':'getStringLowercase','d':'Get Lowercased Text','ed':'Convert all letters found in a string to lowercase','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, Mike!'}]},{'k':'getStringMd5','d':'Get MD5 String','ed':'Convert a string to a MD5 encoded value','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'}]},{'k':'getStringNormalized','d':'Get Normalized String By Field','ed':'Normalize a string depending on the field name','g':'text','p':[{'n':'field','r':true,'t':'options','p':'name','o':[{'name':'Alphabetic','value':'alphabetic'},{'name':'Alphanumeric','value':'alphanumeric'},{'name':'Cif','value':'cif'},{'name':'City','value':'city'},{'name':'Country','value':'country'},{'name':'Date','value':'date'},{'name':'Decimal','value':'decimal'},{'name':'Dni','value':'dni'},{'name':'Domain','value':'domain'},{'name':'Email','value':'email'},{'name':'Gender','value':'gender'},{'name':'Integer','value':'integer'},{'name':'Ip','value':'ip'},{'name':'Mobile','value':'mobile'},{'name':'Name','value':'name'},{'name':'Nie','value':'nie'},{'name':'Nif','value':'nif'},{'name':'Phone','value':'phone'},{'name':'Province','value':'province'},{'name':'Zipcode','value':'zipcode'}]},{'n':'text','r':true,'t':'string','p':'JMª Gº.Fco.gtez. Gro.'}]},{'k':'getStringParsed','d':'Get Parsed Text','ed':'Analyze string and return all emails, phones, zipcodes and links detected','g':'text','p':[{'n':'text','r':true,'t':'string','p':'My email is miquel@uproc.io and my phone is 34605281220'}]},{'k':'checkStringRandom','d':'Check String Is Random','ed':'Check if string contains random characters without sense','g':'text','p':[{'n':'text','r':true,'t':'string','p':'t2 chhsdfitoixcv'}]},{'k':'checkStringRegex','d':'Check String Is Valid By Pattern','ed':'Check if string contains a value that matches with a regular expression','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'},{'n':'regex','r':true,'t':'string','p':'^test$'}]},{'k':'getStringReplaceAll','d':'Get Texts Replaced By String','ed':'Replace all values found in a string by another','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, good morning!'},{'n':'find','r':true,'t':'string','p':'o'},{'n':'replace','r':true,'t':'string','p':'u'}]},{'k':'getStringReplaceFirst','d':'Get Text Replaced By String','ed':'Replace first value found in a string by another','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, good morning!'},{'n':'find','r':true,'t':'string','p':'o'},{'n':'replace','r':true,'t':'string','p':'u'}]},{'k':'getStringSha','d':'Get SHA String','ed':'Convert a string to a SHA encoded value','g':'text','p':[{'n':'text','r':true,'t':'string','p':'test'}]},{'k':'getStringSplit','d':'Get Splitted Values By Separator','ed':'Split a value in two parts, using a separator present in the original string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Texto largo, separado por coma'},{'n':'separator','r':true,'t':'string','p':','}]},{'k':'getStringSplitAndJoin','d':'Get Splitted And Merged Values By Separator','ed':'Split a value in two parts and join them, using a separator present in the original string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, good morning!'},{'n':'separator','r':true,'t':'string','p':','},{'n':'glue','r':true,'t':'string','p':';'}]},{'k':'checkStringStarts','d':'Check String Starts With','ed':'Check if string starts with a character','g':'text','p':[{'n':'text1','r':true,'t':'string','p':'test'},{'n':'text2','r':true,'t':'string','p':'t'}]},{'k':'getStringTranslated','d':'Get Translated Text','ed':'It allows to translate a text to any language','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, my name is Mike'},{'n':'language','r':true,'t':'options','p':'Spanish','o':[{'name':'Afrikaans','value':'Afrikaans'},{'name':'Albanian','value':'Albanian'},{'name':'Amharic','value':'Amharic'},{'name':'Arabic','value':'Arabic'},{'name':'Armenian','value':'Armenian'},{'name':'Azeerbaijani','value':'Azeerbaijani'},{'name':'Basque','value':'Basque'},{'name':'Belarusian','value':'Belarusian'},{'name':'Bengali','value':'Bengali'},{'name':'Bosnian','value':'Bosnian'},{'name':'Bulgarian','value':'Bulgarian'},{'name':'Catalan','value':'Catalan'},{'name':'Cebuano','value':'Cebuano'},{'name':'Chinese (Simplified)','value':'Chinese (Simplified)'},{'name':'Chinese (Traditional)','value':'Chinese (Traditional)'},{'name':'Corsican','value':'Corsican'},{'name':'Croatian','value':'Croatian'},{'name':'Czech','value':'Czech'},{'name':'Danish','value':'Danish'},{'name':'Dutch','value':'Dutch'},{'name':'English','value':'English'},{'name':'Esperanto','value':'Esperanto'},{'name':'Estonian','value':'Estonian'},{'name':'Finnish','value':'Finnish'},{'name':'French','value':'French'},{'name':'Frisian','value':'Frisian'},{'name':'Galician','value':'Galician'},{'name':'Georgian','value':'Georgian'},{'name':'German','value':'German'},{'name':'Greek','value':'Greek'},{'name':'Gujarati','value':'Gujarati'},{'name':'Haitian Creole','value':'Haitian Creole'},{'name':'Hausa','value':'Hausa'},{'name':'Hawaiian','value':'Hawaiian'},{'name':'Hebrew','value':'Hebrew'},{'name':'Hindi','value':'Hindi'},{'name':'Hmong','value':'Hmong'},{'name':'Hungarian','value':'Hungarian'},{'name':'Icelandic','value':'Icelandic'},{'name':'Igbo','value':'Igbo'},{'name':'Indonesian','value':'Indonesian'},{'name':'Irish','value':'Irish'},{'name':'Italian','value':'Italian'},{'name':'Japanese','value':'Japanese'},{'name':'Javanese','value':'Javanese'},{'name':'Kannada','value':'Kannada'},{'name':'Kazakh','value':'Kazakh'},{'name':'Khmer','value':'Khmer'},{'name':'Korean','value':'Korean'},{'name':'Kurdish','value':'Kurdish'},{'name':'Kyrgyz','value':'Kyrgyz'},{'name':'Lao','value':'Lao'},{'name':'Latin','value':'Latin'},{'name':'Latvian','value':'Latvian'},{'name':'Lithuanian','value':'Lithuanian'},{'name':'Luxembourgish','value':'Luxembourgish'},{'name':'Macedonian','value':'Macedonian'},{'name':'Malagasy','value':'Malagasy'},{'name':'Malay','value':'Malay'},{'name':'Malayalam','value':'Malayalam'},{'name':'Maltese','value':'Maltese'},{'name':'Maori','value':'Maori'},{'name':'Marathi','value':'Marathi'},{'name':'Mongolian','value':'Mongolian'},{'name':'Myanmar (Burmese)','value':'Myanmar (Burmese)'},{'name':'Nepali','value':'Nepali'},{'name':'Norwegian','value':'Norwegian'},{'name':'Nyanja (Chichewa)','value':'Nyanja (Chichewa)'},{'name':'Pashto','value':'Pashto'},{'name':'Persian','value':'Persian'},{'name':'Polish','value':'Polish'},{'name':'Portuguese (Portugal, Brazil)','value':'Portuguese (Portugal, Brazil)'},{'name':'Punjabi','value':'Punjabi'},{'name':'Romanian','value':'Romanian'},{'name':'Russian','value':'Russian'},{'name':'Samoan','value':'Samoan'},{'name':'Scots Gaelic','value':'Scots Gaelic'},{'name':'Serbian','value':'Serbian'},{'name':'Sesotho','value':'Sesotho'},{'name':'Shona','value':'Shona'},{'name':'Sindhi','value':'Sindhi'},{'name':'Sinhala (Sinhalese)','value':'Sinhala (Sinhalese)'},{'name':'Slovak','value':'Slovak'},{'name':'Slovenian','value':'Slovenian'},{'name':'Somali','value':'Somali'},{'name':'Spanish','value':'Spanish'},{'name':'Sundanese','value':'Sundanese'},{'name':'Swahili','value':'Swahili'},{'name':'Swedish','value':'Swedish'},{'name':'Tagalog (Filipino)','value':'Tagalog (Filipino)'},{'name':'Tajik','value':'Tajik'},{'name':'Tamil','value':'Tamil'},{'name':'Telugu','value':'Telugu'},{'name':'Thai','value':'Thai'},{'name':'Turkish','value':'Turkish'},{'name':'Ukrainian','value':'Ukrainian'},{'name':'Urdu','value':'Urdu'},{'name':'Uzbek','value':'Uzbek'},{'name':'Vietnamese','value':'Vietnamese'},{'name':'Welsh','value':'Welsh'},{'name':'Xhosa','value':'Xhosa'},{'name':'Yiddish','value':'Yiddish'},{'name':'Yoruba','value':'Yoruba'},{'name':'Zulu','value':'Zulu'}]}]},{'k':'checkStringUppercase','d':'Check Text Is Uppercased','ed':'Check if string only contains uppercase characters','g':'text','p':[{'n':'text','r':true,'t':'string','p':'AAA'}]},{'k':'getStringUppercase','d':'Get Uppercased Text','ed':'Convert all letters found in a string to uppercase','g':'text','p':[{'n':'text','r':true,'t':'string','p':'Hi, Mike!'}]},{'k':'getStringVlookup','d':'Get String VLookup','ed':'Lookup string between multiple values by fuzzy logic and regex patterns','g':'text','p':[{'n':'text','r':true,'t':'string','p':'s4mple'},{'n':'texts','r':true,'t':'string','p':'sample,samples'}]},{'k':'getVatByAddress','d':'Get VAT% By Address','ed':'Get country VAT by address','g':'finance','p':[{'n':'address','r':true,'t':'string','p':'Mallorca, 120 España'}]},{'k':'getVatByCoordinates','d':'Get VAT% By Coordinates','ed':'Get country VAT by coordinates','g':'finance','p':[{'n':'coordinates','r':true,'t':'string','p':'41.61921,2.2904413'}]},{'k':'getVatByIp','d':'Get VAT% By IP','ed':'Get VAT by IP address','g':'finance','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getVatByIsocode','d':'Get VAT% By ISO Code','ed':'Get VAT value by country ISO code','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'ES'}]},{'k':'getVatByNumber','d':'Get Data By TIN (VIES)','ed':'Get related european TIN number in Europe','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'ES'},{'n':'tin','r':true,'t':'string','p':'44016116G'}]},{'k':'getVatByPhone','d':'Get VAT% By Phone','ed':'Get VAT by phone number, with worldwide coverage','g':'finance','p':[{'n':'phone','r':true,'t':'string','p':'57122000111'}]},{'k':'getVatByZipcode','d':'Get VAT% By Zipcode','ed':'Get VAT by zipcode','g':'finance','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'checkVatExist','d':'Check TIN Exists (VIES)','ed':'Check if TIN number exists in Europe','g':'finance','p':[{'n':'isocode','r':true,'t':'string','p':'ES'},{'n':'tin','r':true,'t':'string','p':'44016116G'}]},{'k':'getWordBanned','d':'Get Banned Words','ed':'Discover English banned words in the email body or subject','g':'text','p':[{'n':'text','r':true,'t':'string','p':'I am so thrilled to inform you that our new amazing feature is live!'}]},{'k':'getWordCleanAbuse','d':'Get Text Cleaned Without Abuse Words','ed':'Clean abuse words from a string','g':'text','p':[{'n':'text','r':true,'t':'string','p':'comentario de un maldito personaje'}]},{'k':'getWordCount','d':'Get Words Count','ed':'Count total words in a text','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'}]},{'k':'checkWordCountBetw','d':'Check Word Count Between','ed':'Check if the number of words in a sentence is between two determined quantities','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count1','r':true,'t':'string','p':'1'},{'n':'count2','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountEq','d':'Check Word Count Equal','ed':'Check if the number of words in a sentence equals a certain amount','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountGe','d':'Check Word Count Greater Or Equal','ed':'Check if the number of words in a sentence is greater than or equal to a certain amount','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountGt','d':'Check Word Count Greater','ed':'Check if the number of words in a sentence is greater than a certain amount','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'1'}]},{'k':'checkWordCountLe','d':'Check Word Count Lower Or Equal','ed':'Check if the number of words present in a sentence is less than or equal to a quantity','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'2'}]},{'k':'checkWordCountLt','d':'Check Word Count Lower','ed':'','g':'text','p':[{'n':'text','r':true,'t':'string','p':'sample text'},{'n':'count','r':true,'t':'string','p':'3'}]},{'k':'getZipcodeByIp','d':'Get Zipcode By IP','ed':'Discover the zipcode if you have an IP address','g':'geographic','p':[{'n':'ip','r':true,'t':'string','p':'95.23.100.79'}]},{'k':'getZipcodeByPrefix','d':'Get Zipcode By Prefix (ES)','ed':'Get first zipcode by prefix, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'080'}]},{'k':'checkZipcodeExist','d':'Check Zipcode By Prefix Exists (ES)','ed':'Discover if a zipcode number prefix exists, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'checkZipcodeFormat','d':'Check Zipcode Has Valid Format (ES)','ed':'Discover if a zipcode number has a valid format, only for Spain','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08012'}]},{'k':'getZipcodeListByPrefix','d':'Get Zipcodes By Prefix (ES)','ed':'Get multiple zipcodes by prefix, with worldwide coverage','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'080'}]},{'k':'getZipcodeNormalized','d':'Get Normalized Zipcode','ed':'Allow to normalize a zipcode, removing non allowed characters','g':'geographic','p':[{'n':'zipcode','r':true,'t':'string','p':'08i 12'}]}]}; \ No newline at end of file diff --git a/packages/nodes-base/nodes/UProc/ToolDescription.ts b/packages/nodes-base/nodes/UProc/ToolDescription.ts index 2073a2915d..733a6d08fa 100644 --- a/packages/nodes-base/nodes/UProc/ToolDescription.ts +++ b/packages/nodes-base/nodes/UProc/ToolDescription.ts @@ -1,6 +1,6 @@ import { IDataObject, - INodeProperties, + INodeProperties } from 'n8n-workflow'; import { diff --git a/packages/nodes-base/nodes/UProc/UProc.node.ts b/packages/nodes-base/nodes/UProc/UProc.node.ts index 99f0af76f9..8721f9b166 100644 --- a/packages/nodes-base/nodes/UProc/UProc.node.ts +++ b/packages/nodes-base/nodes/UProc/UProc.node.ts @@ -75,7 +75,7 @@ export class UProc implements INodeType { displayName: 'Data Webhook', name: 'dataWebhook', type: 'string', - description: 'URL to send tool response when tool has resolved your request. You can create your own webhook at Beeceptor, Integromat, Zapier or n8n', + description: 'URL to send tool response when tool has resolved your request. You can create your own webhook at en Beeceptor, Integromat, Zapier or n8n', default: '', }, ], @@ -92,7 +92,6 @@ export class UProc implements INodeType { const tool = this.getNodeParameter('tool', 0) as string; const additionalOptions = this.getNodeParameter('additionalOptions', 0) as IDataObject; - const dataWebhook = additionalOptions.dataWebhook as string; interface LooseObject { diff --git a/packages/nodes-base/nodes/UProc/uProc.node.json b/packages/nodes-base/nodes/UProc/uProc.node.json index 90a6a8b9b8..a64480d3c0 100644 --- a/packages/nodes-base/nodes/UProc/uProc.node.json +++ b/packages/nodes-base/nodes/UProc/uProc.node.json @@ -17,4 +17,4 @@ } ] } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/UProc/uproc.png b/packages/nodes-base/nodes/UProc/uproc.png index 1358a6c3c3..071e993f01 100644 Binary files a/packages/nodes-base/nodes/UProc/uproc.png and b/packages/nodes-base/nodes/UProc/uproc.png differ diff --git a/packages/nodes-base/nodes/Webhook.node.ts b/packages/nodes-base/nodes/Webhook.node.ts index 268a466246..351610fe70 100644 --- a/packages/nodes-base/nodes/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook.node.ts @@ -404,7 +404,7 @@ export class Webhook implements INodeType { // @ts-ignore const mimeType = headers['content-type'] || 'application/json'; if (mimeType.includes('multipart/form-data')) { - const form = new formidable.IncomingForm(); + const form = new formidable.IncomingForm({}); return new Promise((resolve, reject) => { @@ -427,7 +427,7 @@ export class Webhook implements INodeType { binaryPropertyName = `${options.binaryPropertyName}${count}`; } - const fileJson = (files[file] as formidable.File).toJSON() as IDataObject; + const fileJson = (files[file] as formidable.File).toJSON() as unknown as IDataObject; const fileContent = await fs.promises.readFile((files[file] as formidable.File).path); returnItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(Buffer.from(fileContent), fileJson.name as string, fileJson.type as string); diff --git a/packages/nodes-base/nodes/Wise/GenericFunctions.ts b/packages/nodes-base/nodes/Wise/GenericFunctions.ts new file mode 100644 index 0000000000..a4aa44f430 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/GenericFunctions.ts @@ -0,0 +1,164 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to Wise. + */ +export async function wiseApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + option: IDataObject = {}, +) { + const { apiToken, environment } = this.getCredentials('wiseApi') as { + apiToken: string, + environment: 'live' | 'test', + }; + + const rootUrl = environment === 'live' + ? 'https://api.transferwise.com/' + : 'https://api.sandbox.transferwise.tech/'; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'Authorization': `Bearer ${apiToken}`, + }, + method, + uri: `${rootUrl}${endpoint}`, + qs, + body, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + try { + return await this.helpers.request!(options); + } catch (error) { + + const errors = error.error.errors; + + if (errors && Array.isArray(errors)) { + const errorMessage = errors.map((e) => e.message).join(' | '); + throw new Error(`Wise error response [${error.statusCode}]: ${errorMessage}`); + } + + throw new Error(`Wise error response [${error.statusCode}]: ${error}`); + } +} + +/** + * Populate the binary property of node items with binary data for a PDF file. + */ +export async function handleBinaryData( + this: IExecuteFunctions, + items: INodeExecutionData[], + i: number, + endpoint: string, +) { + const data = await wiseApiRequest.call(this, 'GET', endpoint, {}, {}, { encoding: null }); + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + + items[i].binary = items[i].binary ?? {}; + items[i].binary![binaryProperty] = await this.helpers.prepareBinaryData(data); + items[i].binary![binaryProperty].fileName = this.getNodeParameter('fileName', i) as string; + items[i].binary![binaryProperty].fileExtension = 'pdf'; + + return items; +} + +export function getTriggerName(eventName: string) { + const events: IDataObject = { + 'tranferStateChange': 'transfers#state-change', + 'transferActiveCases': 'transfers#active-cases', + 'balanceCredit': 'balances#credit', + }; + return events[eventName]; +} + +export type BorderlessAccount = { + id: number, + balances: Array<{ currency: string }> +}; + +export type ExchangeRateAdditionalFields = { + interval: 'day' | 'hour' | 'minute', + range: { + rangeProperties: { from: string, to: string } + }, + time: string, +}; + +export type Profile = { + id: number, + type: 'business' | 'personal', +}; + +export type Recipient = { + id: number, + accountHolderName: string +}; + +export type StatementAdditionalFields = { + lineStyle: 'COMPACT' | 'FLAT', + range: { + rangeProperties: { intervalStart: string, intervalEnd: string } + }, +}; + +export type TransferFilters = { + [key: string]: string | IDataObject; + range: { + rangeProperties: { createdDateStart: string, createdDateEnd: string } + }, + sourceCurrency: string, + status: string, + targetCurrency: string, +}; + +export const livePublicKey = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvO8vXV+JksBzZAY6GhSO +XdoTCfhXaaiZ+qAbtaDBiu2AGkGVpmEygFmWP4Li9m5+Ni85BhVvZOodM9epgW3F +bA5Q1SexvAF1PPjX4JpMstak/QhAgl1qMSqEevL8cmUeTgcMuVWCJmlge9h7B1CS +D4rtlimGZozG39rUBDg6Qt2K+P4wBfLblL0k4C4YUdLnpGYEDIth+i8XsRpFlogx +CAFyH9+knYsDbR43UJ9shtc42Ybd40Afihj8KnYKXzchyQ42aC8aZ/h5hyZ28yVy +Oj3Vos0VdBIs/gAyJ/4yyQFCXYte64I7ssrlbGRaco4nKF3HmaNhxwyKyJafz19e +HwIDAQAB +-----END PUBLIC KEY-----`; + +export const testPublicKey = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwpb91cEYuyJNQepZAVfP +ZIlPZfNUefH+n6w9SW3fykqKu938cR7WadQv87oF2VuT+fDt7kqeRziTmPSUhqPU +ys/V2Q1rlfJuXbE+Gga37t7zwd0egQ+KyOEHQOpcTwKmtZ81ieGHynAQzsn1We3j +wt760MsCPJ7GMT141ByQM+yW1Bx+4SG3IGjXWyqOWrcXsxAvIXkpUD/jK/L958Cg +nZEgz0BSEh0QxYLITnW1lLokSx/dTianWPFEhMC9BgijempgNXHNfcVirg1lPSyg +z7KqoKUN0oHqWLr2U1A+7kqrl6O2nx3CKs1bj1hToT1+p4kcMoHXA7kA+VBLUpEs +VwIDAQAB +-----END PUBLIC KEY-----`; diff --git a/packages/nodes-base/nodes/Wise/Wise.node.ts b/packages/nodes-base/nodes/Wise/Wise.node.ts new file mode 100644 index 0000000000..dfdf1e7beb --- /dev/null +++ b/packages/nodes-base/nodes/Wise/Wise.node.ts @@ -0,0 +1,514 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + accountFields, + accountOperations, + exchangeRateFields, + exchangeRateOperations, + profileFields, + profileOperations, + quoteFields, + quoteOperations, + recipientFields, + recipientOperations, + transferFields, + transferOperations, +} from './descriptions'; + +import { + BorderlessAccount, + ExchangeRateAdditionalFields, + handleBinaryData, + Profile, + Recipient, + StatementAdditionalFields, + TransferFilters, + wiseApiRequest, +} from './GenericFunctions'; + +import { + omit, +} from 'lodash'; + +import * as moment from 'moment-timezone'; + +import * as uuid from 'uuid/v4'; + +export class Wise implements INodeType { + description: INodeTypeDescription = { + displayName: 'Wise', + name: 'wise', + icon: 'file:wise.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Wise API', + defaults: { + name: 'Wise', + color: '#37517e', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'wiseApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Account', + value: 'account', + }, + { + name: 'Exchange Rate', + value: 'exchangeRate', + }, + { + name: 'Profile', + value: 'profile', + }, + { + name: 'Recipient', + value: 'recipient', + }, + { + name: 'Quote', + value: 'quote', + }, + { + name: 'Transfer', + value: 'transfer', + }, + ], + default: 'account', + description: 'Resource to consume', + }, + ...accountOperations, + ...accountFields, + ...exchangeRateOperations, + ...exchangeRateFields, + ...profileOperations, + ...profileFields, + ...quoteOperations, + ...quoteFields, + ...recipientOperations, + ...recipientFields, + ...transferOperations, + ...transferFields, + ], + }; + + methods = { + loadOptions: { + async getBorderlessAccounts(this: ILoadOptionsFunctions) { + const qs = { + profileId: this.getNodeParameter('profileId', 0), + }; + + const accounts = await wiseApiRequest.call(this, 'GET', 'v1/borderless-accounts', {}, qs); + + return accounts.map(({ id, balances }: BorderlessAccount) => ({ + name: balances.map(({ currency }) => currency).join(' - '), + value: id, + })); + }, + + async getProfiles(this: ILoadOptionsFunctions) { + const profiles = await wiseApiRequest.call(this, 'GET', 'v1/profiles'); + + return profiles.map(({ id, type }: Profile) => ({ + name: type.charAt(0).toUpperCase() + type.slice(1), + value: id, + })); + }, + + async getRecipients(this: ILoadOptionsFunctions) { + const qs = { + profileId: this.getNodeParameter('profileId', 0), + }; + + const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs); + + return recipients.map(({ id, accountHolderName }: Recipient) => ({ + name: accountHolderName, + value: id, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const timezone = this.getTimezone(); + + let responseData; + const returnData: IDataObject[] = []; + let downloadReceipt = false; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'account') { + + // ********************************************************************* + // account + // ********************************************************************* + + if (operation === 'getBalances') { + + // ---------------------------------- + // account: getBalances + // ---------------------------------- + + // https://api-docs.transferwise.com/#borderless-accounts-get-account-balance + + const qs = { + profileId: this.getNodeParameter('profileId', i), + }; + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/borderless-accounts', {}, qs); + + } else if (operation === 'getCurrencies') { + + // ---------------------------------- + // account: getCurrencies + // ---------------------------------- + + // https://api-docs.transferwise.com/#borderless-accounts-get-available-currencies + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/borderless-accounts/balance-currencies'); + + } else if (operation === 'getStatement') { + + // ---------------------------------- + // account: getStatement + // ---------------------------------- + + // https://api-docs.transferwise.com/#borderless-accounts-get-account-statement + + const profileId = this.getNodeParameter('profileId', i); + const borderlessAccountId = this.getNodeParameter('borderlessAccountId', i); + const endpoint = `v3/profiles/${profileId}/borderless-accounts/${borderlessAccountId}/statement.json`; + + const qs = { + currency: this.getNodeParameter('currency', i), + } as IDataObject; + + const { lineStyle, range } = this.getNodeParameter('additionalFields', i) as StatementAdditionalFields; + + if (lineStyle !== undefined) { + qs.type = lineStyle; + } + + if (range !== undefined) { + qs.intervalStart = moment.tz(range.rangeProperties.intervalStart, timezone).utc().format(); + qs.intervalEnd = moment.tz(range.rangeProperties.intervalEnd, timezone).utc().format(); + } else { + qs.intervalStart = moment().subtract(1, 'months').utc().format(); + qs.intervalEnd = moment().utc().format(); + } + + responseData = await wiseApiRequest.call(this, 'GET', endpoint, {}, qs); + + } + + } else if (resource === 'exchangeRate') { + + // ********************************************************************* + // exchangeRate + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // exchangeRate: get + // ---------------------------------- + + // https://api-docs.transferwise.com/#exchange-rates-list + + const qs = { + source: this.getNodeParameter('source', i), + target: this.getNodeParameter('target', i), + } as IDataObject; + + const { + interval, + range, + time, + } = this.getNodeParameter('additionalFields', i) as ExchangeRateAdditionalFields; + + if (interval !== undefined) { + qs.group = interval; + } + + if (time !== undefined) { + qs.time = time; + } + + if (range !== undefined && time === undefined) { + qs.from = moment.tz(range.rangeProperties.from, timezone).utc().format(); + qs.to = moment.tz(range.rangeProperties.to, timezone).utc().format(); + } else { + qs.from = moment().subtract(1, 'months').utc().format(); + qs.to = moment().format(); + } + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/rates', {}, qs); + } + + } else if (resource === 'profile') { + + // ********************************************************************* + // profile + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // profile: get + // ---------------------------------- + + // https://api-docs.transferwise.com/#user-profiles-get-by-id + + const profileId = this.getNodeParameter('profileId', i); + responseData = await wiseApiRequest.call(this, 'GET', `v1/profiles/${profileId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // profile: getAll + // ---------------------------------- + + // https://api-docs.transferwise.com/#user-profiles-list + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/profiles'); + + } + + } else if (resource === 'recipient') { + + // ********************************************************************* + // recipient + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // recipient: getAll + // ---------------------------------- + + // https://api-docs.transferwise.com/#recipient-accounts-list + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/accounts'); + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i); + responseData = responseData.slice(0, limit); + } + } + + } else if (resource === 'quote') { + + // ********************************************************************* + // quote + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // quote: create + // ---------------------------------- + + // https://api-docs.transferwise.com/#quotes-create + + const body = { + profile: this.getNodeParameter('profileId', i), + sourceCurrency: (this.getNodeParameter('sourceCurrency', i) as string).toUpperCase(), + targetCurrency: (this.getNodeParameter('targetCurrency', i) as string).toUpperCase(), + } as IDataObject; + + const amountType = this.getNodeParameter('amountType', i) as 'source' | 'target'; + + if (amountType === 'source') { + body.sourceAmount = this.getNodeParameter('amount', i); + } else if (amountType === 'target') { + body.targetAmount = this.getNodeParameter('amount', i); + } + + responseData = await wiseApiRequest.call(this, 'POST', 'v2/quotes', body, {}); + + } else if (operation === 'get') { + + // ---------------------------------- + // quote: get + // ---------------------------------- + + // https://api-docs.transferwise.com/#quotes-get-by-id + + const quoteId = this.getNodeParameter('quoteId', i); + responseData = await wiseApiRequest.call(this, 'GET', `v2/quotes/${quoteId}`); + } + + } else if (resource === 'transfer') { + + // ********************************************************************* + // transfer + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // transfer: create + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-create + + const body = { + quoteUuid: this.getNodeParameter('quoteId', i), + targetAccount: this.getNodeParameter('targetAccountId', i), + customerTransactionId: uuid(), + } as IDataObject; + + const { reference } = this.getNodeParameter('additionalFields', i) as { reference: string }; + + if (reference !== undefined) { + body.details = { reference }; + } + + responseData = await wiseApiRequest.call(this, 'POST', 'v1/transfers', body, {}); + + } else if (operation === 'delete') { + + // ---------------------------------- + // transfer: delete + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-cancel + + const transferId = this.getNodeParameter('transferId', i); + responseData = await wiseApiRequest.call(this, 'PUT', `v1/transfers/${transferId}/cancel`); + + } else if (operation === 'execute') { + + // ---------------------------------- + // transfer: execute + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-fund + + const profileId = this.getNodeParameter('profileId', i); + const transferId = this.getNodeParameter('transferId', i) as string; + + const endpoint = `v3/profiles/${profileId}/transfers/${transferId}/payments`; + responseData = await wiseApiRequest.call(this, 'POST', endpoint, { type: 'BALANCE' }, {}); + + // in sandbox, simulate transfer completion so that PDF receipt can be downloaded + + const { environment } = this.getCredentials('wiseApi') as IDataObject; + + if (environment === 'test') { + for (const endpoint of ['processing', 'funds_converted', 'outgoing_payment_sent']) { + await wiseApiRequest.call(this, 'GET', `v1/simulation/transfers/${transferId}/${endpoint}`); + } + } + + } else if (operation === 'get') { + + // ---------------------------------- + // transfer: get + // ---------------------------------- + + const transferId = this.getNodeParameter('transferId', i); + downloadReceipt = this.getNodeParameter('downloadReceipt', i) as boolean; + + if (downloadReceipt) { + + // https://api-docs.transferwise.com/#transfers-get-receipt-pdf + + responseData = await handleBinaryData.call(this, items, i, `v1/transfers/${transferId}/receipt.pdf`); + + } else { + + // https://api-docs.transferwise.com/#transfers-get-by-id + + responseData = await wiseApiRequest.call(this, 'GET', `v1/transfers/${transferId}`); + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // transfer: getAll + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-list + + const qs = { + profile: this.getNodeParameter('profileId', i), + } as IDataObject; + + const filters = this.getNodeParameter('filters', i) as TransferFilters; + + Object.keys(omit(filters, 'range')).forEach(key => { + qs[key] = filters[key]; + }); + + if (filters.range !== undefined) { + qs.createdDateStart = moment(filters.range.rangeProperties.createdDateStart).format(); + qs.createdDateEnd = moment(filters.range.rangeProperties.createdDateEnd).format(); + } else { + qs.createdDateStart = moment().subtract(1, 'months').format(); + qs.createdDateEnd = moment().format(); + } + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i); + } + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/transfers', {}, qs); + } + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + if (downloadReceipt && responseData !== undefined) { + return this.prepareOutputData(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Wise/WiseTrigger.node.ts b/packages/nodes-base/nodes/Wise/WiseTrigger.node.ts new file mode 100644 index 0000000000..6b458488ff --- /dev/null +++ b/packages/nodes-base/nodes/Wise/WiseTrigger.node.ts @@ -0,0 +1,189 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + getTriggerName, + livePublicKey, + Profile, + testPublicKey, + wiseApiRequest, +} from './GenericFunctions'; + +import { + createVerify, +} from 'crypto'; + +export class WiseTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Wise Trigger', + name: 'wiseTrigger', + icon: 'file:wise.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle Wise events via webhooks', + defaults: { + name: 'Wise Trigger', + color: '#37517e', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'wiseApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Profile', + name: 'profileId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + default: '', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Balance Credit', + value: 'balanceCredit', + description: 'Triggered every time a balance account is credited.', + }, + { + name: 'Transfer Active Case', + value: 'transferActiveCases', + description: `Triggered every time a transfer's list of active cases is updated.`, + }, + { + name: 'Transfer State Changed', + value: 'tranferStateChange', + description: `Triggered every time a transfer's status is updated.`, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getProfiles(this: ILoadOptionsFunctions) { + const profiles = await wiseApiRequest.call(this, 'GET', 'v1/profiles'); + return profiles.map(({ id, type }: Profile) => ({ + name: type.charAt(0).toUpperCase() + type.slice(1), + value: id, + })); + }, + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const profileId = this.getNodeParameter('profileId') as string; + const event = this.getNodeParameter('event') as string; + const webhooks = await wiseApiRequest.call(this, 'GET', `v3/profiles/${profileId}/subscriptions`); + const trigger = getTriggerName(event); + for (const webhook of webhooks) { + if (webhook.delivery.url === webhookUrl && webhook.scope.id === profileId && webhook.trigger_on === trigger) { + webhookData.webhookId = webhook.id; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const profileId = this.getNodeParameter('profileId') as string; + const event = this.getNodeParameter('event') as string; + const trigger = getTriggerName(event); + const body: IDataObject = { + name: `n8n Webhook`, + trigger_on: trigger, + delivery: { + version: '2.0.0', + url: webhookUrl, + }, + }; + const webhook = await wiseApiRequest.call(this, 'POST', `v3/profiles/${profileId}/subscriptions`, body); + webhookData.webhookId = webhook.id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const profileId = this.getNodeParameter('profileId') as string; + try { + await wiseApiRequest.call(this, 'DELETE', `v3/profiles/${profileId}/subscriptions/${webhookData.webhookId}`); + } catch (error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const headers = this.getHeaderData() as IDataObject; + const credentials = this.getCredentials('wiseApi') as IDataObject; + + if (headers['x-test-notification'] === 'true') { + const res = this.getResponseObject(); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } + + const signature = headers['x-signature'] as string; + + const publicKey = (credentials.environment === 'test') ? testPublicKey : livePublicKey as string; + + //@ts-ignore + const sig = createVerify('RSA-SHA1').update(req.rawBody); + const verified = sig.verify( + publicKey, + signature, + 'base64', + ); + + if (verified === false) { + return {}; + } + + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts new file mode 100644 index 0000000000..5b7fd3d5fa --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts @@ -0,0 +1,195 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const accountOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getBalances', + description: 'Operation to perform', + options: [ + { + name: 'Get Balances', + value: 'getBalances', + description: 'Retrieve balances for all account currencies of this user.', + }, + { + name: 'Get Currencies', + value: 'getCurrencies', + description: 'Retrieve currencies in the borderless account of this user.', + }, + { + name: 'Get Statement', + value: 'getStatement', + description: 'Retrieve the statement for the borderless account of this user.', + }, + ], + displayOptions: { + show: { + resource: [ + 'account', + ], + }, + }, + }, +] as INodeProperties[]; + +export const accountFields = [ + // ---------------------------------- + // account: getBalances + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to retrieve the balance of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getBalances', + ], + }, + }, + }, + + // ---------------------------------- + // account: getStatement + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile whose account to retrieve the statement of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + }, + { + displayName: 'Borderless Account ID', + name: 'borderlessAccountId', + type: 'options', + default: [], + required: true, + typeOptions: { + loadOptionsMethod: 'getBorderlessAccounts', + loadOptionsDependsOn: [ + 'profileId', + ], + }, + description: 'ID of the borderless account to retrieve the statement of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + }, + { + displayName: 'Currency', + name: 'currency', + type: 'string', + default: '', + // TODO: preload + description: 'Code of the currency of the borderless account to retrieve the statement of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + options: [ + { + displayName: 'Line Style', + name: 'lineStyle', + type: 'options', + default: 'COMPACT', + description: 'Line style to retrieve the statement in.', + options: [ + { + name: 'Compact', + value: 'COMPACT', + description: 'Single line per transaction.', + }, + { + name: 'Flat', + value: 'FLAT', + description: 'Separate lines for transaction fees.', + }, + ], + }, + { + displayName: 'Range', + name: 'range', + type: 'fixedCollection', + placeholder: 'Add Range', + default: {}, + options: [ + { + displayName: 'Range Properties', + name: 'rangeProperties', + values: [ + { + displayName: 'Range Start', + name: 'intervalStart', + type: 'dateTime', + default: '', + }, + { + displayName: 'Range End', + name: 'intervalEnd', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts new file mode 100644 index 0000000000..bee99e6fe4 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts @@ -0,0 +1,140 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const exchangeRateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + }, + }, + }, +] as INodeProperties[]; + +export const exchangeRateFields = [ + // ---------------------------------- + // exchangeRate: get + // ---------------------------------- + { + displayName: 'Source Currency', + name: 'source', + type: 'string', + default: '', + description: 'Code of the source currency to retrieve the exchange rate for.', + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Target Currency', + name: 'target', + type: 'string', + default: '', + description: 'Code of the target currency to retrieve the exchange rate for.', + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Interval', + name: 'interval', + type: 'options', + default: 'day', + options: [ + { + name: 'Day', + value: 'day', + }, + { + name: 'Hour', + value: 'hour', + }, + { + name: 'Minute', + value: 'minute', + }, + ], + }, + { + displayName: 'Range', + name: 'range', + type: 'fixedCollection', + placeholder: 'Add Range', + description: 'Range of time to retrieve the exchange rate for.', + default: {}, + options: [ + { + displayName: 'Range Properties', + name: 'rangeProperties', + values: [ + { + displayName: 'Range Start', + name: 'from', + type: 'dateTime', + default: '', + }, + { + displayName: 'Range End', + name: 'to', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Time', + name: 'time', + type: 'dateTime', + default: '', + description: 'Point in time to retrieve the exchange rate for.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts new file mode 100644 index 0000000000..dfa8e88c53 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts @@ -0,0 +1,57 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const profileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'profile', + ], + }, + }, + }, +] as INodeProperties[]; + +export const profileFields = [ + // ---------------------------------- + // profile: get + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to retrieve.', + displayOptions: { + show: { + resource: [ + 'profile', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts new file mode 100644 index 0000000000..6cb5446d87 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts @@ -0,0 +1,181 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const quoteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'quote', + ], + }, + }, + }, +] as INodeProperties[]; + +export const quoteFields = [ + // ---------------------------------- + // quote: create + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to create the quote under.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Account ID', + name: 'targetAccountId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getRecipients', + }, + description: 'ID of the account that will receive the funds.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Amount Type', + name: 'amountType', + type: 'options', + default: 'source', + options: [ + { + name: 'Source', + value: 'source', + }, + { + name: 'Target', + value: 'target', + }, + ], + description: 'Whether the amount is to be sent or received.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Amount', + name: 'amount', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + description: 'Amount of funds for the quote to create.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Source Currency', + name: 'sourceCurrency', + type: 'string', + default: '', + description: 'Code of the currency to send for the quote to create.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Currency', + name: 'targetCurrency', + type: 'string', + default: '', + description: 'Code of the currency to receive for the quote to create.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------- + // quote: get + // ---------------------------------- + { + displayName: 'Quote ID', + name: 'quoteId', + type: 'string', + required: true, + default: '', + description: 'ID of the quote to retrieve.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts new file mode 100644 index 0000000000..9479a8808b --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts @@ -0,0 +1,73 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const recipientOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'recipient', + ], + }, + }, + }, +] as INodeProperties[]; + +export const recipientFields = [ + // ---------------------------------- + // recipient: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts new file mode 100644 index 0000000000..2e81ad5b04 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts @@ -0,0 +1,460 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const transferOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Execute', + value: 'execute', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'transfer', + ], + }, + }, + }, +] as INodeProperties[]; + +export const transferFields = [ + // ---------------------------------- + // transfer: create + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + loadOptionsDependsOn: [ + 'profileId', + ], + }, + description: 'ID of the user profile to retrieve the balance of.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Quote ID', + name: 'quoteId', + type: 'string', + required: true, + default: '', + description: 'ID of the quote based on which to create the transfer.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Account ID', + name: 'targetAccountId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getRecipients', + }, + description: 'ID of the account that will receive the funds.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Reference', + name: 'reference', + type: 'string', + default: '', + description: 'Reference text to show in the recipient\'s bank statement', + }, + ], + }, + + // ---------------------------------- + // transfer: delete + // ---------------------------------- + { + displayName: 'Transfer ID', + name: 'transferId', + type: 'string', + required: true, + default: '', + description: 'ID of the transfer to delete.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // transfer: execute + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to execute the transfer under.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'execute', + ], + }, + }, + }, + { + displayName: 'Transfer ID', + name: 'transferId', + type: 'string', + required: true, + default: '', + description: 'ID of the transfer to execute.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'execute', + ], + }, + }, + }, + + // ---------------------------------- + // transfer: get + // ---------------------------------- + { + displayName: 'Transfer ID', + name: 'transferId', + type: 'string', + required: true, + default: '', + description: 'ID of the transfer to retrieve.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download Receipt', + name: 'downloadReceipt', + type: 'boolean', + required: true, + default: false, + description: 'Download the transfer receipt as a PDF file.
Only for executed transfers, having status \'Outgoing Payment Sent\'.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + downloadReceipt: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + downloadReceipt: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // transfer: getAll + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to retrieve.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Range', + name: 'range', + type: 'fixedCollection', + placeholder: 'Add Range', + description: 'Range of time for filtering the transfers.', + default: {}, + options: [ + { + displayName: 'Range Properties', + name: 'rangeProperties', + values: [ + { + displayName: 'Created Date Start', + name: 'createdDateStart', + type: 'dateTime', + default: '', + }, + { + displayName: 'Created Date End', + name: 'createdDateEnd', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Source Currency', + name: 'sourceCurrency', + type: 'string', + default: '', + description: 'Code of the source currency for filtering the transfers.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'processing', + options: [ + { + name: 'Bounced Back', + value: 'bounced_back', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + { + name: 'Charged Back', + value: 'charged_back', + }, + { + name: 'Outgoing Payment Sent', + value: 'outgoing_payment_sent', + }, + { + name: 'Funds Converted', + value: 'funds_converted', + }, + { + name: 'Funds Refunded', + value: 'funds_refunded', + }, + { + name: 'Incoming Payment Waiting', + value: 'incoming_payment_waiting', + }, + { + name: 'Processing', + value: 'processing', + }, + { + name: 'Unknown', + value: 'unknown', + }, + { + name: 'Waiting for Recipient Input to Proceed', + value: 'waiting_recipient_input_to_proceed', + }, + ], + }, + { + displayName: 'Target Currency', + name: 'targetCurrency', + type: 'string', + default: '', + description: 'Code of the target currency for filtering the transfers.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/index.ts b/packages/nodes-base/nodes/Wise/descriptions/index.ts new file mode 100644 index 0000000000..0e6fc50395 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/index.ts @@ -0,0 +1,6 @@ +export * from './AccountDescription'; +export * from './ExchangeRateDescription'; +export * from './ProfileDescription'; +export * from './QuoteDescription'; +export * from './RecipientDescription'; +export * from './TransferDescription'; diff --git a/packages/nodes-base/nodes/Wise/wise.svg b/packages/nodes-base/nodes/Wise/wise.svg new file mode 100644 index 0000000000..d0dfa43fad --- /dev/null +++ b/packages/nodes-base/nodes/Wise/wise.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/nodes-base/nodes/WriteBinaryFile.node.ts b/packages/nodes-base/nodes/WriteBinaryFile.node.ts index eb56a006fd..9794283529 100644 --- a/packages/nodes-base/nodes/WriteBinaryFile.node.ts +++ b/packages/nodes-base/nodes/WriteBinaryFile.node.ts @@ -1,6 +1,7 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, + IExecuteSingleFunctions } from 'n8n-core'; import { IDataObject, @@ -53,39 +54,53 @@ export class WriteBinaryFile implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { - const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; - const fileName = this.getNodeParameter('fileName') as string; + const items = this.getInputData(); - if (item.binary === undefined) { - throw new Error('No binary data set. So file can not be written!'); + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; + + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; + + const fileName = this.getNodeParameter('fileName', itemIndex) as string; + + item = items[itemIndex]; + + if (item.binary === undefined) { + throw new Error('No binary data set. So file can not be written!'); + } + + if (item.binary[dataPropertyName] === undefined) { + throw new Error(`The binary property "${dataPropertyName}" does not exist. So no file can be written!`); + } + + // Write the file to disk + await fsWriteFileAsync(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary'); + + const newItem: INodeExecutionData = { + json: {}, + }; + Object.assign(newItem.json, item.json); + + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + newItem.binary = {}; + Object.assign(newItem.binary, item.binary); + } + + // Add the file name to data + + (newItem.json as IDataObject).fileName = fileName; + + returnData.push(newItem); } - - if (item.binary[dataPropertyName] === undefined) { - throw new Error(`The binary property "${dataPropertyName}" does not exist. So no file can be written!`); - } - - // Write the file to disk - await fsWriteFileAsync(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary'); - - const newItem: INodeExecutionData = { - json: {}, - }; - Object.assign(newItem.json, item.json); - - if (item.binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - newItem.binary = {}; - Object.assign(newItem.binary, item.binary); - } - - // Add the file name to data - (newItem.json as IDataObject).fileName = fileName; - - return newItem; + return this.prepareOutputData(returnData); } + } diff --git a/packages/nodes-base/nodes/Xero/Xero.node.ts b/packages/nodes-base/nodes/Xero/Xero.node.ts index 3b5f20547c..27369c647e 100644 --- a/packages/nodes-base/nodes/Xero/Xero.node.ts +++ b/packages/nodes-base/nodes/Xero/Xero.node.ts @@ -41,7 +41,7 @@ export class Xero implements INodeType { description: INodeTypeDescription = { displayName: 'Xero', name: 'xero', - icon: 'file:xero.png', + icon: 'file:xero.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', diff --git a/packages/nodes-base/nodes/Xero/xero.png b/packages/nodes-base/nodes/Xero/xero.png deleted file mode 100644 index 61b8f10c8c..0000000000 Binary files a/packages/nodes-base/nodes/Xero/xero.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Xero/xero.svg b/packages/nodes-base/nodes/Xero/xero.svg new file mode 100644 index 0000000000..ca25aa1e74 --- /dev/null +++ b/packages/nodes-base/nodes/Xero/xero.svg @@ -0,0 +1,16 @@ + + + xero_node_icon + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6d91522707..cb7e05155c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.104.0", + "version": "0.111.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -35,7 +35,9 @@ "dist/credentials/Amqp.credentials.js", "dist/credentials/AsanaApi.credentials.js", "dist/credentials/AsanaOAuth2Api.credentials.js", + "dist/credentials/ApiTemplateIoApi.credentials.js", "dist/credentials/AutomizyApi.credentials.js", + "dist/credentials/AutopilotApi.credentials.js", "dist/credentials/Aws.credentials.js", "dist/credentials/AffinityApi.credentials.js", "dist/credentials/BannerbearApi.credentials.js", @@ -43,8 +45,10 @@ "dist/credentials/BitbucketApi.credentials.js", "dist/credentials/BitlyApi.credentials.js", "dist/credentials/BitlyOAuth2Api.credentials.js", + "dist/credentials/BitwardenApi.credentials.js", "dist/credentials/BoxOAuth2Api.credentials.js", "dist/credentials/BrandfetchApi.credentials.js", + "dist/credentials/BubbleApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", @@ -61,6 +65,8 @@ "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/DeepLApi.credentials.js", + "dist/credentials/DemioApi.credentials.js", "dist/credentials/DiscourseApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", @@ -68,6 +74,8 @@ "dist/credentials/DropboxApi.credentials.js", "dist/credentials/DropboxOAuth2Api.credentials.js", "dist/credentials/EgoiApi.credentials.js", + "dist/credentials/EmeliaApi.credentials.js", + "dist/credentials/ERPNextApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", "dist/credentials/EventbriteOAuth2Api.credentials.js", "dist/credentials/FacebookGraphApi.credentials.js", @@ -96,10 +104,12 @@ "dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", + "dist/credentials/GoogleSlidesOAuth2Api.credentials.js", "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", "dist/credentials/GoogleTasksOAuth2Api.credentials.js", "dist/credentials/GoogleTranslateOAuth2Api.credentials.js", "dist/credentials/GotifyApi.credentials.js", + "dist/credentials/GoToWebinarOAuth2Api.credentials.js", "dist/credentials/YouTubeOAuth2Api.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", @@ -122,6 +132,7 @@ "dist/credentials/JotFormApi.credentials.js", "dist/credentials/Kafka.credentials.js", "dist/credentials/KeapOAuth2Api.credentials.js", + "dist/credentials/LemlistApi.credentials.js", "dist/credentials/LineNotifyOAuth2Api.credentials.js", "dist/credentials/LingvaNexApi.credentials.js", "dist/credentials/LinkedInOAuth2Api.credentials.js", @@ -160,6 +171,7 @@ "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/OrbitApi.credentials.js", + "dist/credentials/OuraApi.credentials.js", "dist/credentials/PaddleApi.credentials.js", "dist/credentials/PagerDutyApi.credentials.js", "dist/credentials/PagerDutyOAuth2Api.credentials.js", @@ -169,7 +181,9 @@ "dist/credentials/PipedriveApi.credentials.js", "dist/credentials/PipedriveOAuth2Api.credentials.js", "dist/credentials/PhilipsHueOAuth2Api.credentials.js", + "dist/credentials/PlivoApi.credentials.js", "dist/credentials/Postgres.credentials.js", + "dist/credentials/PostHogApi.credentials.js", "dist/credentials/PostmarkApi.credentials.js", "dist/credentials/ProfitWellApi.credentials.js", "dist/credentials/PushbulletOAuth2Api.credentials.js", @@ -177,7 +191,9 @@ "dist/credentials/PushcutApi.credentials.js", "dist/credentials/QuestDb.credentials.js", "dist/credentials/QuickBaseApi.credentials.js", + "dist/credentials/QuickBooksOAuth2Api.credentials.js", "dist/credentials/RabbitMQ.credentials.js", + "dist/credentials/RaindropOAuth2Api.credentials.js", "dist/credentials/RedditOAuth2Api.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", @@ -238,6 +254,7 @@ "dist/credentials/WebflowApi.credentials.js", "dist/credentials/WebflowOAuth2Api.credentials.js", "dist/credentials/WekanApi.credentials.js", + "dist/credentials/WiseApi.credentials.js", "dist/credentials/WooCommerceApi.credentials.js", "dist/credentials/WordpressApi.credentials.js", "dist/credentials/WufooApi.credentials.js", @@ -251,6 +268,7 @@ "dist/credentials/ZulipApi.credentials.js" ], "nodes": [ + "dist/nodes/ActivationTrigger.node.js", "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", "dist/nodes/AgileCrm/AgileCrm.node.js", @@ -261,23 +279,29 @@ "dist/nodes/Amqp/AmqpTrigger.node.js", "dist/nodes/Asana/Asana.node.js", "dist/nodes/Asana/AsanaTrigger.node.js", + "dist/nodes/ApiTemplateIo/ApiTemplateIo.node.js", "dist/nodes/Affinity/Affinity.node.js", "dist/nodes/Affinity/AffinityTrigger.node.js", "dist/nodes/Automizy/Automizy.node.js", + "dist/nodes/Autopilot/Autopilot.node.js", + "dist/nodes/Autopilot/AutopilotTrigger.node.js", "dist/nodes/Aws/AwsLambda.node.js", "dist/nodes/Aws/Comprehend/AwsComprehend.node.js", "dist/nodes/Aws/Rekognition/AwsRekognition.node.js", "dist/nodes/Aws/S3/AwsS3.node.js", "dist/nodes/Aws/SES/AwsSes.node.js", + "dist/nodes/Aws/SQS/AwsSqs.node.js", "dist/nodes/Aws/AwsSns.node.js", "dist/nodes/Aws/AwsSnsTrigger.node.js", "dist/nodes/Bannerbear/Bannerbear.node.js", "dist/nodes/Beeminder/Beeminder.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitly/Bitly.node.js", + "dist/nodes/Bitwarden/Bitwarden.node.js", "dist/nodes/Box/Box.node.js", "dist/nodes/Box/BoxTrigger.node.js", "dist/nodes/Brandfetch/Brandfetch.node.js", + "dist/nodes/Bubble/Bubble.node.js", "dist/nodes/Calendly/CalendlyTrigger.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", @@ -294,6 +318,7 @@ "dist/nodes/Contentful/Contentful.node.js", "dist/nodes/ConvertKit/ConvertKit.node.js", "dist/nodes/ConvertKit/ConvertKitTrigger.node.js", + "dist/nodes/Copper/Copper.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cortex/Cortex.node.js", "dist/nodes/CrateDb/CrateDb.node.js", @@ -302,6 +327,8 @@ "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", + "dist/nodes/DeepL/DeepL.node.js", + "dist/nodes/Demio/Demio.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Discourse/Discourse.node.js", "dist/nodes/Disqus/Disqus.node.js", @@ -311,7 +338,10 @@ "dist/nodes/Egoi/Egoi.node.js", "dist/nodes/EmailReadImap.node.js", "dist/nodes/EmailSend.node.js", + "dist/nodes/Emelia/Emelia.node.js", + "dist/nodes/Emelia/EmeliaTrigger.node.js", "dist/nodes/ErrorTrigger.node.js", + "dist/nodes/ERPNext/ERPNext.node.js", "dist/nodes/Eventbrite/EventbriteTrigger.node.js", "dist/nodes/ExecuteCommand.node.js", "dist/nodes/ExecuteWorkflow.node.js", @@ -342,10 +372,12 @@ "dist/nodes/Google/Gmail/Gmail.node.js", "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", + "dist/nodes/Google/Slides/GoogleSlides.node.js", "dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/Google/Translate/GoogleTranslate.node.js", "dist/nodes/Google/YouTube/YouTube.node.js", "dist/nodes/Gotify/Gotify.node.js", + "dist/nodes/GoToWebinar/GoToWebinar.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/HackerNews/HackerNews.node.js", @@ -371,6 +403,8 @@ "dist/nodes/Kafka/KafkaTrigger.node.js", "dist/nodes/Keap/Keap.node.js", "dist/nodes/Keap/KeapTrigger.node.js", + "dist/nodes/Lemlist/Lemlist.node.js", + "dist/nodes/Lemlist/LemlistTrigger.node.js", "dist/nodes/Line/Line.node.js", "dist/nodes/LingvaNex/LingvaNex.node.js", "dist/nodes/LinkedIn/LinkedIn.node.js", @@ -409,6 +443,7 @@ "dist/nodes/OpenThesaurus/OpenThesaurus.node.js", "dist/nodes/OpenWeatherMap.node.js", "dist/nodes/Orbit/Orbit.node.js", + "dist/nodes/Oura/Oura.node.js", "dist/nodes/Paddle/Paddle.node.js", "dist/nodes/PagerDuty/PagerDuty.node.js", "dist/nodes/PayPal/PayPal.node.js", @@ -418,7 +453,9 @@ "dist/nodes/Pipedrive/Pipedrive.node.js", "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/PhilipsHue/PhilipsHue.node.js", + "dist/nodes/Plivo/Plivo.node.js", "dist/nodes/Postgres/Postgres.node.js", + "dist/nodes/PostHog/PostHog.node.js", "dist/nodes/Postmark/PostmarkTrigger.node.js", "dist/nodes/ProfitWell/ProfitWell.node.js", "dist/nodes/Pushbullet/Pushbullet.node.js", @@ -427,7 +464,9 @@ "dist/nodes/Pushover/Pushover.node.js", "dist/nodes/QuestDb/QuestDb.node.js", "dist/nodes/QuickBase/QuickBase.node.js", + "dist/nodes/QuickBooks/QuickBooks.node.js", "dist/nodes/RabbitMQ/RabbitMQ.node.js", + "dist/nodes/Raindrop/Raindrop.node.js", "dist/nodes/RabbitMQ/RabbitMQTrigger.node.js", "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js", @@ -498,6 +537,8 @@ "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Wufoo/WufooTrigger.node.js", + "dist/nodes/Wise/Wise.node.js", + "dist/nodes/Wise/WiseTrigger.node.js", "dist/nodes/Xero/Xero.node.js", "dist/nodes/Xml.node.js", "dist/nodes/Yourls/Yourls.node.js", @@ -535,12 +576,13 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.51.0", + "n8n-workflow": "~0.55.0", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~3.9.7" }, "dependencies": { + "@types/lossless-json": "^1.0.0", "@types/promise-ftp": "^1.3.4", "@types/snowflake-sdk": "^1.5.1", "amqplib": "^0.6.0", @@ -555,6 +597,7 @@ "get-system-fonts": "^2.0.2", "glob-promise": "^3.4.0", "gm": "^1.23.1", + "iconv-lite": "^0.6.2", "imap-simple": "^4.3.0", "iso-639-1": "^2.1.3", "jsonwebtoken": "^8.5.1", @@ -562,14 +605,15 @@ "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "lodash.unset": "^4.5.2", + "lossless-json": "^1.0.4", "mailparser": "^2.8.1", "moment": "2.28.0", "moment-timezone": "^0.5.28", - "mongodb": "^3.5.5", + "mongodb": "3.6.3", "mqtt": "4.2.1", "mssql": "^6.2.0", "mysql2": "~2.1.0", - "n8n-core": "~0.62.0", + "n8n-core": "~0.67.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg": "^8.3.0", diff --git a/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.js b/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.js new file mode 100644 index 0000000000..bc3bcbb758 --- /dev/null +++ b/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.js @@ -0,0 +1,158 @@ +const PostgresFun = require('../../../nodes/Postgres/Postgres.node.functions') +const pgPromise = require('pg-promise'); + +describe('pgUpdate', () => { + it('runs query to update db', async () => { + const updateItem = {id: 1234, name: 'test'}; + const nodeParams = { + table: 'mytable', + schema: 'myschema', + updateKey: 'id', + columns: 'id,name' + }; + const getNodeParam = (key) => nodeParams[key]; + const pgp = pgPromise(); + const none = jest.fn(); + const db = {none}; + + const items = [ + { + json: updateItem + } + ]; + + const results = await PostgresFun.pgUpdate(getNodeParam, pgp, db, items) + + expect(db.none).toHaveBeenCalledWith(`update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values(1234,'test')) as v(\"id\",\"name\") WHERE v.id = t.id`); + expect(results).toEqual([updateItem]); + }); + + it('runs query to update db if updateKey is not in columns', async () => { + const updateItem = {id: 1234, name: 'test'}; + const nodeParams = { + table: 'mytable', + schema: 'myschema', + updateKey: 'id', + columns: 'name' + }; + const getNodeParam = (key) => nodeParams[key]; + const pgp = pgPromise(); + const none = jest.fn(); + const db = {none}; + + const items = [ + { + json: updateItem + } + ]; + + const results = await PostgresFun.pgUpdate(getNodeParam, pgp, db, items) + + expect(db.none).toHaveBeenCalledWith(`update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values(1234,'test')) as v(\"id\",\"name\") WHERE v.id = t.id`); + expect(results).toEqual([updateItem]); + }); + + it('runs query to update db with cast as updateKey', async () => { + const updateItem = {id: '1234', name: 'test'}; + const nodeParams = { + table: 'mytable', + schema: 'myschema', + updateKey: 'id:uuid', + columns: 'name' + }; + const getNodeParam = (key) => nodeParams[key]; + const pgp = pgPromise(); + const none = jest.fn(); + const db = {none}; + + const items = [ + { + json: updateItem + } + ]; + + const results = await PostgresFun.pgUpdate(getNodeParam, pgp, db, items) + + expect(db.none).toHaveBeenCalledWith(`update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values('1234'::uuid,'test')) as v(\"id\",\"name\") WHERE v.id = t.id`); + expect(results).toEqual([updateItem]); + }); + + it('runs query to update db with cast in target columns', async () => { + const updateItem = {id: '1234', name: 'test'}; + const nodeParams = { + table: 'mytable', + schema: 'myschema', + updateKey: 'id', + columns: 'id:uuid,name' + }; + const getNodeParam = (key) => nodeParams[key]; + const pgp = pgPromise(); + const none = jest.fn(); + const db = {none}; + + const items = [ + { + json: updateItem + } + ]; + + const results = await PostgresFun.pgUpdate(getNodeParam, pgp, db, items) + + expect(db.none).toHaveBeenCalledWith(`update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values('1234'::uuid,'test')) as v(\"id\",\"name\") WHERE v.id = t.id`); + expect(results).toEqual([updateItem]); + }); +}); + + + +describe('pgInsert', () => { + it('runs query to insert', async () => { + const insertItem = {id: 1234, name: 'test', age: 34}; + const nodeParams = { + table: 'mytable', + schema: 'myschema', + columns: 'id,name,age', + returnFields: '*', + }; + const getNodeParam = (key) => nodeParams[key]; + const pgp = pgPromise(); + const manyOrNone = jest.fn(); + const db = {manyOrNone}; + + const items = [ + { + json: insertItem, + }, + ]; + + const results = await PostgresFun.pgInsert(getNodeParam, pgp, db, items); + + expect(db.manyOrNone).toHaveBeenCalledWith(`insert into \"myschema\".\"mytable\"(\"id\",\"name\",\"age\") values(1234,'test',34) RETURNING *`); + expect(results).toEqual([undefined, [insertItem]]); + }); + + it('runs query to insert with type casting', async () => { + const insertItem = {id: 1234, name: 'test', age: 34}; + const nodeParams = { + table: 'mytable', + schema: 'myschema', + columns: 'id:int,name:text,age', + returnFields: '*', + }; + const getNodeParam = (key) => nodeParams[key]; + const pgp = pgPromise(); + const manyOrNone = jest.fn(); + const db = {manyOrNone}; + + const items = [ + { + json: insertItem, + }, + ]; + + const results = await PostgresFun.pgInsert(getNodeParam, pgp, db, items); + + expect(db.manyOrNone).toHaveBeenCalledWith(`insert into \"myschema\".\"mytable\"(\"id\",\"name\",\"age\") values(1234::int,'test'::text,34) RETURNING *`); + expect(results).toEqual([undefined, [insertItem]]); + }); +}); diff --git a/packages/workflow/package.json b/packages/workflow/package.json index b43779998a..170f4c640e 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,59 +1,59 @@ { - "name": "n8n-workflow", - "version": "0.51.0", - "description": "Workflow base code of n8n", - "license": "SEE LICENSE IN LICENSE.md", - "homepage": "https://n8n.io", - "author": { - "name": "Jan Oberhauser", - "email": "jan@n8n.io" + "name": "n8n-workflow", + "version": "0.55.0", + "description": "Workflow base code of n8n", + "license": "SEE LICENSE IN LICENSE.md", + "homepage": "https://n8n.io", + "author": { + "name": "Jan Oberhauser", + "email": "jan@n8n.io" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/n8n-io/n8n.git" + }, + "main": "dist/src/index", + "types": "dist/src/index.d.ts", + "scripts": { + "dev": "npm run watch", + "build": "tsc", + "tslint": "tslint -p tsconfig.json -c tslint.json", + "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", + "watch": "tsc --watch", + "test": "jest" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@types/express": "^4.17.6", + "@types/jest": "^26.0.13", + "@types/lodash.get": "^4.4.6", + "@types/node": "14.0.27", + "jest": "^26.4.2", + "ts-jest": "^26.3.0", + "tslint": "^6.1.2", + "typescript": "~3.9.7" + }, + "dependencies": { + "lodash.get": "^4.4.2", + "riot-tmpl": "^3.0.8" + }, + "jest": { + "transform": { + "^.+\\.tsx?$": "ts-jest" }, - "repository": { - "type": "git", - "url": "git+https://github.com/n8n-io/n8n.git" - }, - "main": "dist/src/index", - "types": "dist/src/index.d.ts", - "scripts": { - "dev": "npm run watch", - "build": "tsc", - "tslint": "tslint -p tsconfig.json -c tslint.json", - "tslintfix": "tslint --fix -p tsconfig.json -c tslint.json", - "watch": "tsc --watch", - "test": "jest" - }, - "files": [ - "dist" + "testURL": "http://localhost/", + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "testPathIgnorePatterns": [ + "/dist/", + "/node_modules/" ], - "devDependencies": { - "@types/express": "^4.17.6", - "@types/jest": "^26.0.13", - "@types/lodash.get": "^4.4.6", - "@types/node": "14.0.27", - "jest": "^26.4.2", - "ts-jest": "^26.3.0", - "tslint": "^6.1.2", - "typescript": "~3.9.7" - }, - "dependencies": { - "lodash.get": "^4.4.2", - "riot-tmpl": "^3.0.8" - }, - "jest": { - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testURL": "http://localhost/", - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "testPathIgnorePatterns": [ - "/dist/", - "/node_modules/" - ], - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json" - ] - } + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ] + } } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index fc1498651f..b57837379e 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -9,6 +9,7 @@ export interface IBinaryData { data: string; mimeType: string; fileName?: string; + directory?: string; fileExtension?: string; } @@ -17,6 +18,7 @@ export interface IOAuth2Options { property?: string; tokenType?: string; keepBearer?: boolean; + tokenExpiredStatusCode?: number; } export interface IConnection { @@ -163,11 +165,11 @@ export interface IDataObject { export interface IGetExecutePollFunctions { - (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IPollFunctions; + (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IPollFunctions; } export interface IGetExecuteTriggerFunctions { - (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions; + (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): ITriggerFunctions; } @@ -182,7 +184,7 @@ export interface IGetExecuteSingleFunctions { export interface IGetExecuteHookFunctions { - (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions; + (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean, webhookData?: IWebhookData): IHookFunctions; } @@ -269,6 +271,7 @@ export interface ILoadOptionsFunctions { export interface IHookFunctions { getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getMode(): WorkflowExecuteMode; + getActivationMode(): WorkflowActivateMode; getNode(): INode; getNodeWebhookUrl: (name: string) => string | undefined; getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any @@ -286,6 +289,7 @@ export interface IPollFunctions { __emit(data: INodeExecutionData[][]): void; getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getMode(): WorkflowExecuteMode; + getActivationMode(): WorkflowActivateMode; getNode(): INode; getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any getRestApiUrl(): string; @@ -301,6 +305,7 @@ export interface ITriggerFunctions { emit(data: INodeExecutionData[][]): void; getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getMode(): WorkflowExecuteMode; + getActivationMode(): WorkflowActivateMode; getNode(): INode; getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any getRestApiUrl(): string; @@ -441,7 +446,7 @@ export interface INodeProperties { default: NodeParameterValue | INodeParameters | INodeParameters[] | NodeParameterValue[]; description?: string; displayOptions?: IDisplayOptions; - options?: Array; + options?: Array; placeholder?: string; isNodeSetting?: boolean; noDataExpression?: boolean; @@ -735,7 +740,7 @@ export interface IWorkflowExecuteAdditionalData { credentials: IWorkflowCredentials; credentialsHelper: ICredentialsHelper; encryptionKey: string; - executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]) => Promise; // tslint:disable-line:no-any + executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: any) => Promise; // tslint:disable-line:no-any // hooks?: IWorkflowExecuteHooks; hooks?: WorkflowHooks; httpResponse?: express.Response; @@ -744,10 +749,11 @@ export interface IWorkflowExecuteAdditionalData { timezone: string; webhookBaseUrl: string; webhookTestBaseUrl: string; - currentNodeParameters? : INodeParameters; + currentNodeParameters?: INodeParameters; } export type WorkflowExecuteMode = 'cli' | 'error' | 'integrated' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook'; +export type WorkflowActivateMode = 'init' | 'create' | 'update' | 'activate' | 'manual'; export interface IWorkflowHooksOptionalParameters { parentProcessMode?: string; diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index d221ba28de..026fa80086 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -24,6 +24,7 @@ import { NodeParameterValue, ObservableObject, WebhookSetupMethodNames, + WorkflowActivateMode, WorkflowExecuteMode, } from './'; @@ -769,7 +770,7 @@ export class Workflow { * @returns {(Promise)} * @memberof Workflow */ - async runWebhookMethod(method: WebhookSetupMethodNames, webhookData: IWebhookData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode, isTest?: boolean): Promise { + async runWebhookMethod(method: WebhookSetupMethodNames, webhookData: IWebhookData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, isTest?: boolean): Promise { const node = this.getNode(webhookData.node) as INode; const nodeType = this.nodeTypes.getByName(node.type) as INodeType; @@ -786,7 +787,7 @@ export class Workflow { return; } - const thisArgs = nodeExecuteFunctions.getExecuteHookFunctions(this, node, webhookData.workflowExecuteAdditionalData, mode, isTest, webhookData); + const thisArgs = nodeExecuteFunctions.getExecuteHookFunctions(this, node, webhookData.workflowExecuteAdditionalData, mode, activation, isTest, webhookData); return nodeType.webhookMethods[webhookData.webhookDescription.name][method]!.call(thisArgs); } @@ -802,8 +803,8 @@ export class Workflow { * @returns {(Promise)} * @memberof Workflow */ - async runTrigger(node: INode, getTriggerFunctions: IGetExecuteTriggerFunctions, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): Promise { - const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode); + async runTrigger(node: INode, getTriggerFunctions: IGetExecuteTriggerFunctions, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise { + const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode, activation); const nodeType = this.nodeTypes.getByName(node.type); @@ -982,7 +983,7 @@ export class Workflow { } else if (nodeType.poll) { if (mode === 'manual') { // In manual mode run the poll function - const thisArgs = nodeExecuteFunctions.getExecutePollFunctions(this, node, additionalData, mode); + const thisArgs = nodeExecuteFunctions.getExecutePollFunctions(this, node, additionalData, mode, 'manual'); return nodeType.poll.call(thisArgs); } else { // In any other mode pass data through as it already contains the result of the poll @@ -991,7 +992,7 @@ export class Workflow { } else if (nodeType.trigger) { if (mode === 'manual') { // In manual mode start the trigger - const triggerResponse = await this.runTrigger(node, nodeExecuteFunctions.getExecuteTriggerFunctions, additionalData, mode); + const triggerResponse = await this.runTrigger(node, nodeExecuteFunctions.getExecuteTriggerFunctions, additionalData, mode, 'manual'); if (triggerResponse === undefined) { return null;