mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
Merge branch 'master' of https://github.com/n8n-io/n8n into n8n-io-master
This commit is contained in:
commit
9f4e18eb8d
85
.github/workflows/test-workflows.yml
vendored
Normal file
85
.github/workflows/test-workflows.yml
vendored
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
name: Run test workflows
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 2 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-test-workflows:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x]
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
path: n8n
|
||||||
|
-
|
||||||
|
name: Checkout workflows repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: n8n-io/test-workflows
|
||||||
|
path: test-workflows
|
||||||
|
-
|
||||||
|
name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
-
|
||||||
|
name: npm install and build
|
||||||
|
run: |
|
||||||
|
cd n8n
|
||||||
|
npm install
|
||||||
|
npm run bootstrap
|
||||||
|
npm run build --if-present
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
shell: bash
|
||||||
|
-
|
||||||
|
name: Import credentials
|
||||||
|
run: n8n/packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||||
|
-
|
||||||
|
name: Import workflows
|
||||||
|
run: n8n/packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||||
|
-
|
||||||
|
name: Copy static assets
|
||||||
|
run: |
|
||||||
|
cp n8n/assets/n8n-logo.png /tmp/n8n-logo.png
|
||||||
|
cp n8n/assets/n8n-screenshot.png /tmp/n8n-screenshot.png
|
||||||
|
cp n8n/node_modules/pdf-parse/test/data/05-versions-space.pdf /tmp/05-versions-space.pdf
|
||||||
|
cp n8n/node_modules/pdf-parse/test/data/04-valid.pdf /tmp/04-valid.pdf
|
||||||
|
shell: bash
|
||||||
|
-
|
||||||
|
name: Run tests
|
||||||
|
run: n8n/packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.txt --shortOutput --concurrency=16 --compare=test-workflows/snapshots
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||||
|
-
|
||||||
|
name: Export credentials
|
||||||
|
if: always()
|
||||||
|
run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||||
|
-
|
||||||
|
name: Commit and push credential changes
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
cd test-workflows
|
||||||
|
git config --global user.name 'n8n test bot'
|
||||||
|
git config --global user.email 'n8n-test-bot@users.noreply.github.com'
|
||||||
|
git commit -am "Automated credential update"
|
||||||
|
git push --force --quiet "https://janober:${{ secrets.TOKEN }}@github.com/n8n-io/test-workflows.git" main:main
|
|
@ -7,7 +7,7 @@
|
||||||
"build": "lerna exec npm run build",
|
"build": "lerna exec npm run build",
|
||||||
"dev": "lerna exec npm run dev --parallel",
|
"dev": "lerna exec npm run dev --parallel",
|
||||||
"clean:dist": "lerna exec -- rimraf ./dist",
|
"clean:dist": "lerna exec -- rimraf ./dist",
|
||||||
"optimize-svg": "find ./packages -name '*.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
||||||
"start": "run-script-os",
|
"start": "run-script-os",
|
||||||
"start:default": "cd packages/cli/bin && ./n8n",
|
"start:default": "cd packages/cli/bin && ./n8n",
|
||||||
"start:windows": "cd packages/cli/bin && n8n",
|
"start:windows": "cd packages/cli/bin && n8n",
|
||||||
|
|
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
This list shows all the versions which include breaking changes and how to upgrade.
|
This list shows all the versions which include breaking changes and how to upgrade.
|
||||||
|
|
||||||
|
## 0.130.0
|
||||||
|
|
||||||
|
### What changed?
|
||||||
|
|
||||||
|
For the Taiga regular and trigger nodes, the server and cloud credentials types are now unified into a single credentials type and the `version` param has been removed. Also, the `issue:create` operation now automatically loads the tags as `multiOptions`.
|
||||||
|
|
||||||
|
### When is action necessary?
|
||||||
|
|
||||||
|
If you are using the Taiga nodes, reconnect the credentials. If you are using tags in the `issue:create` operation, reselect them.
|
||||||
|
|
||||||
|
## 0.127.0
|
||||||
|
|
||||||
|
### What changed?
|
||||||
|
|
||||||
|
For the Zoho node, the `lead:create` operation now requires a "Company" parameter, the parameter "Address" is now inside "Additional Options", and the parameters "Title" and "Is Duplicate Record" were removed. Also, the `lead:delete` operation now returns only the `id` of the deleted lead.
|
||||||
|
|
||||||
|
### When is action necessary?
|
||||||
|
|
||||||
|
If you are using `lead:create` with "Company" or "Address", reset the parameters; for the other two parameters, no action needed. If you are using the response from `lead:delete`, reselect the `id` key.
|
||||||
|
|
||||||
## 0.118.0
|
## 0.118.0
|
||||||
|
|
||||||
### What changed?
|
### What changed?
|
||||||
|
|
54
packages/cli/commands/Interfaces.d.ts
vendored
Normal file
54
packages/cli/commands/Interfaces.d.ts
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
interface IResult {
|
||||||
|
totalWorkflows: number;
|
||||||
|
summary: {
|
||||||
|
failedExecutions: number,
|
||||||
|
successfulExecutions: number,
|
||||||
|
warningExecutions: number,
|
||||||
|
errors: IExecutionError[],
|
||||||
|
warnings: IExecutionError[],
|
||||||
|
};
|
||||||
|
coveredNodes: {
|
||||||
|
[nodeType: string]: number
|
||||||
|
};
|
||||||
|
executions: IExecutionResult[];
|
||||||
|
}
|
||||||
|
interface IExecutionResult {
|
||||||
|
workflowId: string | number;
|
||||||
|
workflowName: string;
|
||||||
|
executionTime: number; // Given in seconds with decimals for milisseconds
|
||||||
|
finished: boolean;
|
||||||
|
executionStatus: ExecutionStatus;
|
||||||
|
error?: string;
|
||||||
|
changes?: string;
|
||||||
|
coveredNodes: {
|
||||||
|
[nodeType: string]: number
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IExecutionError {
|
||||||
|
workflowId: string | number;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWorkflowExecutionProgress {
|
||||||
|
workflowId: string | number;
|
||||||
|
status: ExecutionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeSpecialCases {
|
||||||
|
[nodeName: string]: INodeSpecialCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeSpecialCase {
|
||||||
|
ignoredProperties?: string[];
|
||||||
|
capResults?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecutionStatus = 'success' | 'error' | 'warning' | 'running';
|
||||||
|
|
||||||
|
declare module 'json-diff' {
|
||||||
|
interface IDiffOptions {
|
||||||
|
keysOnly?: boolean;
|
||||||
|
}
|
||||||
|
export function diff(obj1: unknown, obj2: unknown, diffOptions: IDiffOptions): string;
|
||||||
|
}
|
|
@ -46,6 +46,9 @@ export class Execute extends Command {
|
||||||
id: flags.string({
|
id: flags.string({
|
||||||
description: 'id of the workflow to execute',
|
description: 'id of the workflow to execute',
|
||||||
}),
|
}),
|
||||||
|
rawOutput: flags.boolean({
|
||||||
|
description: 'Outputs only JSON data, with no other text',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,10 +186,11 @@ export class Execute extends Command {
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (flags.rawOutput === undefined) {
|
||||||
console.info('Execution was successful:');
|
this.log('Execution was successful:');
|
||||||
console.info('====================================');
|
this.log('====================================');
|
||||||
console.info(JSON.stringify(data, null, 2));
|
}
|
||||||
|
this.log(JSON.stringify(data, null, 2));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error executing workflow. See log messages for details.');
|
console.error('Error executing workflow. See log messages for details.');
|
||||||
logger.error('\nExecution error:');
|
logger.error('\nExecution error:');
|
||||||
|
|
796
packages/cli/commands/executeBatch.ts
Normal file
796
packages/cli/commands/executeBatch.ts
Normal file
|
@ -0,0 +1,796 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
flags,
|
||||||
|
} from '@oclif/command';
|
||||||
|
|
||||||
|
import {
|
||||||
|
UserSettings,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
INode,
|
||||||
|
INodeExecutionData,
|
||||||
|
ITaskData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveExecutions,
|
||||||
|
CredentialsOverwrites,
|
||||||
|
CredentialTypes,
|
||||||
|
Db,
|
||||||
|
ExternalHooks,
|
||||||
|
IExecutionsCurrentSummary,
|
||||||
|
IWorkflowDb,
|
||||||
|
IWorkflowExecutionDataProcess,
|
||||||
|
LoadNodesAndCredentials,
|
||||||
|
NodeTypes,
|
||||||
|
WorkflowCredentials,
|
||||||
|
WorkflowRunner,
|
||||||
|
} from '../src';
|
||||||
|
|
||||||
|
import {
|
||||||
|
sep,
|
||||||
|
} from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
diff,
|
||||||
|
} from 'json-diff';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLogger,
|
||||||
|
} from '../src/Logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoggerProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class ExecuteBatch extends Command {
|
||||||
|
static description = '\nExecutes multiple workflows once';
|
||||||
|
|
||||||
|
static cancelled = false;
|
||||||
|
|
||||||
|
static workflowExecutionsProgress: IWorkflowExecutionProgress[][];
|
||||||
|
|
||||||
|
static shallow = false;
|
||||||
|
|
||||||
|
static compare: string;
|
||||||
|
|
||||||
|
static snapshot: string;
|
||||||
|
|
||||||
|
static concurrency = 1;
|
||||||
|
|
||||||
|
static debug = false;
|
||||||
|
|
||||||
|
static executionTimeout = 3 * 60 * 1000;
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
`$ n8n executeAll`,
|
||||||
|
`$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`,
|
||||||
|
`$ n8n executeAll --debug --output=/data/output.json`,
|
||||||
|
`$ n8n executeAll --ids=10,13,15 --shortOutput`,
|
||||||
|
`$ n8n executeAll --snapshot=/data/snapshots --shallow`,
|
||||||
|
`$ n8n executeAll --compare=/data/previousExecutionData --retries=2`,
|
||||||
|
];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
help: flags.help({ char: 'h' }),
|
||||||
|
debug: flags.boolean({
|
||||||
|
description: 'Toggles on displaying all errors and debug messages.',
|
||||||
|
}),
|
||||||
|
ids: flags.string({
|
||||||
|
description: 'Specifies workflow IDs to get executed, separated by a comma.',
|
||||||
|
}),
|
||||||
|
concurrency: flags.integer({
|
||||||
|
default: 1,
|
||||||
|
description: 'How many workflows can run in parallel. Defaults to 1 which means no concurrency.',
|
||||||
|
}),
|
||||||
|
output: flags.string({
|
||||||
|
description: 'Enable execution saving, You must inform an existing folder to save execution via this param',
|
||||||
|
}),
|
||||||
|
snapshot: flags.string({
|
||||||
|
description: 'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.',
|
||||||
|
}),
|
||||||
|
compare: flags.string({
|
||||||
|
description: 'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.',
|
||||||
|
}),
|
||||||
|
shallow: flags.boolean({
|
||||||
|
description: 'Compares only if attributes output from node are the same, with no regards to neste JSON objects.',
|
||||||
|
}),
|
||||||
|
skipList: flags.string({
|
||||||
|
description: 'File containing a comma separated list of workflow IDs to skip.',
|
||||||
|
}),
|
||||||
|
retries: flags.integer({
|
||||||
|
description: 'Retries failed workflows up to N tries. Default is 1. Set 0 to disable.',
|
||||||
|
default: 1,
|
||||||
|
}),
|
||||||
|
shortOutput: flags.boolean({
|
||||||
|
description: 'Omits the full execution information from output, displaying only summary.',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully handles exit.
|
||||||
|
* @param {boolean} skipExit Whether to skip exit or number according to received signal
|
||||||
|
*/
|
||||||
|
static async stopProcess(skipExit: boolean | number = false) {
|
||||||
|
|
||||||
|
if (ExecuteBatch.cancelled === true) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecuteBatch.cancelled = true;
|
||||||
|
const activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||||
|
const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async execution => {
|
||||||
|
activeExecutionsInstance.stopExecution(execution.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(stopPromises);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(0);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
executingWorkflows = activeExecutionsInstance.getActiveExecutions();
|
||||||
|
}
|
||||||
|
// We may receive true but when called from `process.on`
|
||||||
|
// we get the signal (SIGNIT, etc.)
|
||||||
|
if (skipExit !== true) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatJsonOutput(data: object) {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeConsideredAsWarning(errorMessage: string) {
|
||||||
|
|
||||||
|
const warningStrings = [
|
||||||
|
'refresh token is invalid',
|
||||||
|
'unable to connect to',
|
||||||
|
'econnreset',
|
||||||
|
'429',
|
||||||
|
'econnrefused',
|
||||||
|
'missing a required parameter',
|
||||||
|
];
|
||||||
|
|
||||||
|
errorMessage = errorMessage.toLowerCase();
|
||||||
|
|
||||||
|
for (let i = 0; i < warningStrings.length; i++) {
|
||||||
|
if (errorMessage.includes(warningStrings[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
|
||||||
|
process.on('SIGTERM', ExecuteBatch.stopProcess);
|
||||||
|
process.on('SIGINT', ExecuteBatch.stopProcess);
|
||||||
|
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
const { flags } = this.parse(ExecuteBatch);
|
||||||
|
|
||||||
|
ExecuteBatch.debug = flags.debug === true;
|
||||||
|
ExecuteBatch.concurrency = flags.concurrency || 1;
|
||||||
|
|
||||||
|
const ids: number[] = [];
|
||||||
|
const skipIds: number[] = [];
|
||||||
|
|
||||||
|
if (flags.snapshot !== undefined) {
|
||||||
|
if (fs.existsSync(flags.snapshot)) {
|
||||||
|
if (!fs.lstatSync(flags.snapshot).isDirectory()) {
|
||||||
|
console.log(`The parameter --snapshot must be an existing directory`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`The parameter --snapshot must be an existing directory`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecuteBatch.snapshot = flags.snapshot;
|
||||||
|
}
|
||||||
|
if (flags.compare !== undefined) {
|
||||||
|
if (fs.existsSync(flags.compare)) {
|
||||||
|
if (!fs.lstatSync(flags.compare).isDirectory()) {
|
||||||
|
console.log(`The parameter --compare must be an existing directory`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`The parameter --compare must be an existing directory`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecuteBatch.compare = flags.compare;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.output !== undefined) {
|
||||||
|
if (fs.existsSync(flags.output)) {
|
||||||
|
if (fs.lstatSync(flags.output).isDirectory()) {
|
||||||
|
console.log(`The parameter --output must be a writable file`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.ids !== undefined) {
|
||||||
|
const paramIds = flags.ids.split(',');
|
||||||
|
const re = /\d+/;
|
||||||
|
const matchedIds = paramIds.filter(id => id.match(re)).map(id => parseInt(id.trim(), 10));
|
||||||
|
|
||||||
|
if (matchedIds.length === 0) {
|
||||||
|
console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ids.push(...matchedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.skipList !== undefined) {
|
||||||
|
if (fs.existsSync(flags.skipList)) {
|
||||||
|
const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' });
|
||||||
|
skipIds.push(...contents.split(',').map(id => parseInt(id.trim(), 10)));
|
||||||
|
} else {
|
||||||
|
console.log('Skip list file not found. Exiting.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.shallow === true) {
|
||||||
|
ExecuteBatch.shallow = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Start directly with the init of the database to improve startup time
|
||||||
|
const startDbInitPromise = Db.init();
|
||||||
|
|
||||||
|
// Load all node and credential types
|
||||||
|
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
||||||
|
const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init();
|
||||||
|
|
||||||
|
// Make sure the settings exist
|
||||||
|
await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
|
// Wait till the database is ready
|
||||||
|
await startDbInitPromise;
|
||||||
|
|
||||||
|
let allWorkflows;
|
||||||
|
|
||||||
|
const query = Db.collections!.Workflow!.createQueryBuilder('workflows');
|
||||||
|
|
||||||
|
if (ids.length > 0) {
|
||||||
|
query.andWhere(`workflows.id in (:...ids)`, { ids });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipIds.length > 0) {
|
||||||
|
query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
allWorkflows = await query.getMany() as IWorkflowDb[];
|
||||||
|
|
||||||
|
if (ExecuteBatch.debug === true) {
|
||||||
|
process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait till the n8n-packages have been read
|
||||||
|
await loadNodesAndCredentialsPromise;
|
||||||
|
|
||||||
|
// Load the credentials overwrites if any exist
|
||||||
|
await CredentialsOverwrites().init();
|
||||||
|
|
||||||
|
// Load all external hooks
|
||||||
|
const externalHooks = ExternalHooks();
|
||||||
|
await externalHooks.init();
|
||||||
|
|
||||||
|
// Add the found types to an instance other parts of the application can use
|
||||||
|
const nodeTypes = NodeTypes();
|
||||||
|
await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
|
||||||
|
const credentialTypes = CredentialTypes();
|
||||||
|
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
|
||||||
|
|
||||||
|
// Send a shallow copy of allWorkflows so we still have all workflow data.
|
||||||
|
const results = await this.runTests([...allWorkflows]);
|
||||||
|
|
||||||
|
let { retries } = flags;
|
||||||
|
|
||||||
|
while (retries > 0 && (results.summary.warningExecutions + results.summary.failedExecutions > 0) && ExecuteBatch.cancelled === false) {
|
||||||
|
const failedWorkflowIds = results.summary.errors.map(execution => execution.workflowId);
|
||||||
|
failedWorkflowIds.push(...results.summary.warnings.map(execution => execution.workflowId));
|
||||||
|
|
||||||
|
const newWorkflowList = allWorkflows.filter(workflow => failedWorkflowIds.includes(workflow.id));
|
||||||
|
|
||||||
|
const retryResults = await this.runTests(newWorkflowList);
|
||||||
|
|
||||||
|
this.mergeResults(results, retryResults);
|
||||||
|
// By now, `results` has been updated with the new successful executions.
|
||||||
|
retries--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.output !== undefined) {
|
||||||
|
fs.writeFileSync(flags.output, this.formatJsonOutput(results));
|
||||||
|
console.log('\nExecution finished.');
|
||||||
|
console.log('Summary:');
|
||||||
|
console.log(`\tSuccess: ${results.summary.successfulExecutions}`);
|
||||||
|
console.log(`\tFailures: ${results.summary.failedExecutions}`);
|
||||||
|
console.log(`\tWarnings: ${results.summary.warningExecutions}`);
|
||||||
|
console.log('\nNodes successfully tested:');
|
||||||
|
Object.entries(results.coveredNodes).forEach(([nodeName, nodeCount]) => {
|
||||||
|
console.log(`\t${nodeName}: ${nodeCount}`);
|
||||||
|
});
|
||||||
|
console.log('\nCheck the JSON file for more details.');
|
||||||
|
} else {
|
||||||
|
if (flags.shortOutput === true) {
|
||||||
|
console.log(this.formatJsonOutput({ ...results, executions: results.executions.filter(execution => execution.executionStatus !== 'success') }));
|
||||||
|
} else {
|
||||||
|
console.log(this.formatJsonOutput(results));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteBatch.stopProcess(true);
|
||||||
|
|
||||||
|
if (results.summary.failedExecutions > 0) {
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
this.exit(0);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeResults(results: IResult, retryResults: IResult) {
|
||||||
|
|
||||||
|
if (retryResults.summary.successfulExecutions === 0) {
|
||||||
|
// Nothing to replace.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find successful executions and replace them on previous result.
|
||||||
|
retryResults.executions.forEach(newExecution => {
|
||||||
|
if (newExecution.executionStatus === 'success') {
|
||||||
|
// Remove previous execution from list.
|
||||||
|
results.executions = results.executions.filter(previousExecutions => previousExecutions.workflowId !== newExecution.workflowId);
|
||||||
|
|
||||||
|
const errorIndex = results.summary.errors.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId);
|
||||||
|
if (errorIndex !== -1) {
|
||||||
|
// This workflow errored previously. Decrement error count.
|
||||||
|
results.summary.failedExecutions--;
|
||||||
|
// Remove from the list of errors.
|
||||||
|
results.summary.errors.splice(errorIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const warningIndex = results.summary.warnings.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId);
|
||||||
|
if (warningIndex !== -1) {
|
||||||
|
// This workflow errored previously. Decrement error count.
|
||||||
|
results.summary.warningExecutions--;
|
||||||
|
// Remove from the list of errors.
|
||||||
|
results.summary.warnings.splice(warningIndex, 1);
|
||||||
|
}
|
||||||
|
// Increment successful executions count and push it to all executions array.
|
||||||
|
results.summary.successfulExecutions++;
|
||||||
|
results.executions.push(newExecution);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async runTests(allWorkflows: IWorkflowDb[]): Promise<IResult> {
|
||||||
|
const result: IResult = {
|
||||||
|
totalWorkflows: allWorkflows.length,
|
||||||
|
summary: {
|
||||||
|
failedExecutions: 0,
|
||||||
|
warningExecutions: 0,
|
||||||
|
successfulExecutions: 0,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
coveredNodes: {},
|
||||||
|
executions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ExecuteBatch.debug) {
|
||||||
|
this.initializeLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(async (res) => {
|
||||||
|
const promisesArray = [];
|
||||||
|
for (let i = 0; i < ExecuteBatch.concurrency; i++) {
|
||||||
|
const promise = new Promise(async (resolve) => {
|
||||||
|
let workflow: IWorkflowDb | undefined;
|
||||||
|
while (allWorkflows.length > 0) {
|
||||||
|
workflow = allWorkflows.shift();
|
||||||
|
if (ExecuteBatch.cancelled === true) {
|
||||||
|
process.stdout.write(`Thread ${i + 1} resolving and quitting.`);
|
||||||
|
resolve(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// This if shouldn't be really needed
|
||||||
|
// but it's a concurrency precaution.
|
||||||
|
if (workflow === undefined) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExecuteBatch.debug) {
|
||||||
|
ExecuteBatch.workflowExecutionsProgress[i].push({
|
||||||
|
workflowId: workflow.id,
|
||||||
|
status: 'running',
|
||||||
|
});
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.startThread(workflow).then((executionResult) => {
|
||||||
|
if (ExecuteBatch.debug) {
|
||||||
|
ExecuteBatch.workflowExecutionsProgress[i].pop();
|
||||||
|
}
|
||||||
|
result.executions.push(executionResult);
|
||||||
|
if (executionResult.executionStatus === 'success') {
|
||||||
|
if (ExecuteBatch.debug) {
|
||||||
|
ExecuteBatch.workflowExecutionsProgress[i].push({
|
||||||
|
workflowId: workflow!.id,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
result.summary.successfulExecutions++;
|
||||||
|
const nodeNames = Object.keys(executionResult.coveredNodes);
|
||||||
|
|
||||||
|
nodeNames.map(nodeName => {
|
||||||
|
if (result.coveredNodes[nodeName] === undefined) {
|
||||||
|
result.coveredNodes[nodeName] = 0;
|
||||||
|
}
|
||||||
|
result.coveredNodes[nodeName] += executionResult.coveredNodes[nodeName];
|
||||||
|
});
|
||||||
|
} else if (executionResult.executionStatus === 'warning') {
|
||||||
|
result.summary.warningExecutions++;
|
||||||
|
result.summary.warnings.push({
|
||||||
|
workflowId: executionResult.workflowId,
|
||||||
|
error: executionResult.error!,
|
||||||
|
});
|
||||||
|
if (ExecuteBatch.debug) {
|
||||||
|
ExecuteBatch.workflowExecutionsProgress[i].push({
|
||||||
|
workflowId: workflow!.id,
|
||||||
|
status: 'warning',
|
||||||
|
});
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
} else if (executionResult.executionStatus === 'error') {
|
||||||
|
result.summary.failedExecutions++;
|
||||||
|
result.summary.errors.push({
|
||||||
|
workflowId: executionResult.workflowId,
|
||||||
|
error: executionResult.error!,
|
||||||
|
});
|
||||||
|
if (ExecuteBatch.debug) {
|
||||||
|
ExecuteBatch.workflowExecutionsProgress[i].push({
|
||||||
|
workflowId: workflow!.id,
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Wrong execution status - cannot proceed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
promisesArray.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(promisesArray);
|
||||||
|
|
||||||
|
res(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus() {
|
||||||
|
|
||||||
|
if (ExecuteBatch.cancelled === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.stdout.isTTY === true) {
|
||||||
|
process.stdout.moveCursor(0, - (ExecuteBatch.concurrency));
|
||||||
|
process.stdout.cursorTo(0);
|
||||||
|
process.stdout.clearLine(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => {
|
||||||
|
let message = `${index + 1}: `;
|
||||||
|
concurrentThread.map((executionItem, workflowIndex) => {
|
||||||
|
let openColor = '\x1b[0m';
|
||||||
|
const closeColor = '\x1b[0m';
|
||||||
|
switch (executionItem.status) {
|
||||||
|
case 'success':
|
||||||
|
openColor = '\x1b[32m';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
openColor = '\x1b[31m';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
openColor = '\x1b[33m';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
message += (workflowIndex > 0 ? ', ' : '') + `${openColor}${executionItem.workflowId}${closeColor}`;
|
||||||
|
});
|
||||||
|
if (process.stdout.isTTY === true) {
|
||||||
|
process.stdout.cursorTo(0);
|
||||||
|
process.stdout.clearLine(0);
|
||||||
|
}
|
||||||
|
process.stdout.write(message + '\n');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeLogs() {
|
||||||
|
process.stdout.write('**********************************************\n');
|
||||||
|
process.stdout.write(' n8n test workflows\n');
|
||||||
|
process.stdout.write('**********************************************\n');
|
||||||
|
process.stdout.write('\n');
|
||||||
|
process.stdout.write('Batch number:\n');
|
||||||
|
ExecuteBatch.workflowExecutionsProgress = [];
|
||||||
|
for (let i = 0; i < ExecuteBatch.concurrency; i++) {
|
||||||
|
ExecuteBatch.workflowExecutionsProgress.push([]);
|
||||||
|
process.stdout.write(`${i + 1}: \n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startThread(workflowData: IWorkflowDb): Promise<IExecutionResult> {
|
||||||
|
// This will be the object returned by the promise.
|
||||||
|
// It will be updated according to execution progress below.
|
||||||
|
const executionResult: IExecutionResult = {
|
||||||
|
workflowId: workflowData.id,
|
||||||
|
workflowName: workflowData.name,
|
||||||
|
executionTime: 0,
|
||||||
|
finished: false,
|
||||||
|
executionStatus: 'running',
|
||||||
|
coveredNodes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||||
|
let startNode: INode | undefined = undefined;
|
||||||
|
for (const node of workflowData.nodes) {
|
||||||
|
if (requiredNodeTypes.includes(node.type)) {
|
||||||
|
startNode = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a cool feature here.
|
||||||
|
// On each node, on the Settings tab in the node editor you can change
|
||||||
|
// the `Notes` field to add special cases for comparison and snapshots.
|
||||||
|
// You need to set one configuration per line with the following possible keys:
|
||||||
|
// CAP_RESULTS_LENGTH=x where x is a number. Cap the number of rows from this node to x.
|
||||||
|
// This means if you set CAP_RESULTS_LENGTH=1 we will have only 1 row in the output
|
||||||
|
// IGNORED_PROPERTIES=x,y,z where x, y and z are JSON property names. Removes these
|
||||||
|
// properties from the JSON object (useful for optional properties that can
|
||||||
|
// cause the comparison to detect changes when not true).
|
||||||
|
const nodeEdgeCases = {} as INodeSpecialCases;
|
||||||
|
workflowData.nodes.forEach(node => {
|
||||||
|
executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1;
|
||||||
|
if (node.notes !== undefined && node.notes !== '') {
|
||||||
|
node.notes.split('\n').forEach(note => {
|
||||||
|
const parts = note.split('=');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
if (nodeEdgeCases[node.name] === undefined) {
|
||||||
|
nodeEdgeCases[node.name] = {} as INodeSpecialCase;
|
||||||
|
}
|
||||||
|
if (parts[0] === 'CAP_RESULTS_LENGTH') {
|
||||||
|
nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10);
|
||||||
|
} else if (parts[0] === 'IGNORED_PROPERTIES') {
|
||||||
|
nodeEdgeCases[node.name].ignoredProperties = parts[1].split(',').map(property => property.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
if (startNode === undefined) {
|
||||||
|
// If the workflow does not contain a start-node we can not know what
|
||||||
|
// should be executed and with which data to start.
|
||||||
|
executionResult.error = 'Workflow cannot be started as it does not contain a "Start" node.';
|
||||||
|
executionResult.executionStatus = 'warning';
|
||||||
|
resolve(executionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
let gotCancel = false;
|
||||||
|
|
||||||
|
// Timeouts execution after 5 minutes.
|
||||||
|
const timeoutTimer = setTimeout(() => {
|
||||||
|
gotCancel = true;
|
||||||
|
executionResult.error = 'Workflow execution timed out.';
|
||||||
|
executionResult.executionStatus = 'warning';
|
||||||
|
resolve(executionResult);
|
||||||
|
}, ExecuteBatch.executionTimeout);
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
||||||
|
|
||||||
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
|
credentials,
|
||||||
|
executionMode: 'cli',
|
||||||
|
startNodes: [startNode!.name],
|
||||||
|
workflowData: workflowData!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflowRunner = new WorkflowRunner();
|
||||||
|
const executionId = await workflowRunner.run(runData);
|
||||||
|
|
||||||
|
const activeExecutions = ActiveExecutions.getInstance();
|
||||||
|
const data = await activeExecutions.getPostExecutePromise(executionId);
|
||||||
|
if (gotCancel || ExecuteBatch.cancelled === true) {
|
||||||
|
clearTimeout(timeoutTimer);
|
||||||
|
// The promise was settled already so we simply ignore.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === undefined) {
|
||||||
|
executionResult.error = 'Workflow did not return any data.';
|
||||||
|
executionResult.executionStatus = 'error';
|
||||||
|
} else {
|
||||||
|
executionResult.executionTime = (Date.parse(data.stoppedAt as unknown as string) - Date.parse(data.startedAt as unknown as string)) / 1000;
|
||||||
|
executionResult.finished = (data?.finished !== undefined) as boolean;
|
||||||
|
|
||||||
|
if (data.data.resultData.error) {
|
||||||
|
executionResult.error =
|
||||||
|
data.data.resultData.error.hasOwnProperty('description') ?
|
||||||
|
// @ts-ignore
|
||||||
|
data.data.resultData.error.description : data.data.resultData.error.message;
|
||||||
|
if (data.data.resultData.lastNodeExecuted !== undefined) {
|
||||||
|
executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`;
|
||||||
|
}
|
||||||
|
executionResult.executionStatus = 'error';
|
||||||
|
|
||||||
|
if (this.shouldBeConsideredAsWarning(executionResult.error || '')) {
|
||||||
|
executionResult.executionStatus = 'warning';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ExecuteBatch.shallow === true) {
|
||||||
|
// What this does is guarantee that top-level attributes
|
||||||
|
// from the JSON are kept and the are the same type.
|
||||||
|
|
||||||
|
// We convert nested JSON objects to a simple {object:true}
|
||||||
|
// and we convert nested arrays to ['json array']
|
||||||
|
|
||||||
|
// This reduces the chance of false positives but may
|
||||||
|
// result in not detecting deeper changes.
|
||||||
|
Object.keys(data.data.resultData.runData).map((nodeName: string) => {
|
||||||
|
data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
|
||||||
|
if (taskData.data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(taskData.data).map(connectionName => {
|
||||||
|
const connection = taskData.data![connectionName] as Array<INodeExecutionData[] | null>;
|
||||||
|
connection.map(executionDataArray => {
|
||||||
|
if (executionDataArray === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].capResults !== undefined) {
|
||||||
|
executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
|
||||||
|
}
|
||||||
|
|
||||||
|
executionDataArray.map(executionData => {
|
||||||
|
if (executionData.json === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
|
||||||
|
nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonProperties = executionData.json;
|
||||||
|
|
||||||
|
const nodeOutputAttributes = Object.keys(jsonProperties);
|
||||||
|
nodeOutputAttributes.map(attributeName => {
|
||||||
|
if (Array.isArray(jsonProperties[attributeName])) {
|
||||||
|
jsonProperties[attributeName] = ['json array'];
|
||||||
|
} else if (typeof jsonProperties[attributeName] === 'object') {
|
||||||
|
jsonProperties[attributeName] = { object: true };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If not using shallow comparison then we only treat nodeEdgeCases.
|
||||||
|
const specialCases = Object.keys(nodeEdgeCases);
|
||||||
|
|
||||||
|
specialCases.forEach(nodeName => {
|
||||||
|
data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
|
||||||
|
if (taskData.data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(taskData.data).map(connectionName => {
|
||||||
|
const connection = taskData.data![connectionName] as Array<INodeExecutionData[] | null>;
|
||||||
|
connection.map(executionDataArray => {
|
||||||
|
if (executionDataArray === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeEdgeCases[nodeName].capResults !== undefined) {
|
||||||
|
executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
|
||||||
|
executionDataArray.map(executionData => {
|
||||||
|
if (executionData.json === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedData = this.formatJsonOutput(data);
|
||||||
|
if (ExecuteBatch.compare === undefined) {
|
||||||
|
executionResult.executionStatus = 'success';
|
||||||
|
} else {
|
||||||
|
const fileName = (ExecuteBatch.compare.endsWith(sep) ? ExecuteBatch.compare : ExecuteBatch.compare + sep) + `${workflowData.id}-snapshot.json`;
|
||||||
|
if (fs.existsSync(fileName) === true) {
|
||||||
|
|
||||||
|
const contents = fs.readFileSync(fileName, { encoding: 'utf-8' });
|
||||||
|
|
||||||
|
const changes = diff(JSON.parse(contents), data, { keysOnly: true });
|
||||||
|
|
||||||
|
if (changes !== undefined) {
|
||||||
|
// we have structural changes. Report them.
|
||||||
|
executionResult.error = `Workflow may contain breaking changes`;
|
||||||
|
executionResult.changes = changes;
|
||||||
|
executionResult.executionStatus = 'error';
|
||||||
|
} else {
|
||||||
|
executionResult.executionStatus = 'success';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
executionResult.error = 'Snapshot for not found.';
|
||||||
|
executionResult.executionStatus = 'warning';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Save snapshots only after comparing - this is to make sure we're updating
|
||||||
|
// After comparing to existing verion.
|
||||||
|
if (ExecuteBatch.snapshot !== undefined) {
|
||||||
|
const fileName = (ExecuteBatch.snapshot.endsWith(sep) ? ExecuteBatch.snapshot : ExecuteBatch.snapshot + sep) + `${workflowData.id}-snapshot.json`;
|
||||||
|
fs.writeFileSync(fileName, serializedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
executionResult.error = 'Workflow failed to execute.';
|
||||||
|
executionResult.executionStatus = 'error';
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutTimer);
|
||||||
|
resolve(executionResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -65,6 +65,9 @@ export class ImportCredentialsCommand extends Command {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
|
||||||
|
// Make sure the settings exist
|
||||||
|
await UserSettings.prepareUserSettings();
|
||||||
let i;
|
let i;
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
|
|
@ -18,6 +18,9 @@ import {
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'glob-promise';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
UserSettings,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
export class ImportWorkflowsCommand extends Command {
|
export class ImportWorkflowsCommand extends Command {
|
||||||
static description = 'Import workflows';
|
static description = 'Import workflows';
|
||||||
|
@ -60,6 +63,9 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
|
||||||
|
// Make sure the settings exist
|
||||||
|
await UserSettings.prepareUserSettings();
|
||||||
let i;
|
let i;
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json');
|
const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json');
|
||||||
|
|
67
packages/cli/commands/list/workflow.ts
Normal file
67
packages/cli/commands/list/workflow.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
flags,
|
||||||
|
} from '@oclif/command';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Db,
|
||||||
|
} from "../../src";
|
||||||
|
|
||||||
|
|
||||||
|
export class ListWorkflowCommand extends Command {
|
||||||
|
static description = '\nList workflows';
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
'$ n8n list:workflow',
|
||||||
|
'$ n8n list:workflow --active=true --onlyId',
|
||||||
|
'$ n8n list:workflow --active=false',
|
||||||
|
];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
help: flags.help({ char: 'h' }),
|
||||||
|
active: flags.string({
|
||||||
|
description: 'Filters workflows by active status. Can be true or false',
|
||||||
|
}),
|
||||||
|
onlyId: flags.boolean({
|
||||||
|
description: 'Outputs workflow IDs only, one per line.',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { flags } = this.parse(ListWorkflowCommand);
|
||||||
|
|
||||||
|
if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) {
|
||||||
|
this.error('The --active flag has to be passed using true or false');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Db.init();
|
||||||
|
|
||||||
|
const findQuery: IDataObject = {};
|
||||||
|
if (flags.active !== undefined) {
|
||||||
|
findQuery.active = flags.active === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflows = await Db.collections.Workflow!.find(findQuery);
|
||||||
|
if (flags.onlyId) {
|
||||||
|
workflows.forEach(workflow => console.log(workflow.id));
|
||||||
|
} else {
|
||||||
|
workflows.forEach(workflow => console.log(workflow.id + "|" + workflow.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('\nGOT ERROR');
|
||||||
|
console.log('====================================');
|
||||||
|
console.error(e.message);
|
||||||
|
console.error(e.stack);
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.126.0",
|
"version": "0.129.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -82,6 +82,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oclif/command": "^1.5.18",
|
"@oclif/command": "^1.5.18",
|
||||||
"@oclif/errors": "^1.2.2",
|
"@oclif/errors": "^1.2.2",
|
||||||
|
"@types/json-diff": "^0.5.1",
|
||||||
"@types/jsonwebtoken": "^8.5.2",
|
"@types/jsonwebtoken": "^8.5.2",
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
@ -101,15 +102,16 @@
|
||||||
"glob-promise": "^3.4.0",
|
"glob-promise": "^3.4.0",
|
||||||
"google-timezones-json": "^1.0.2",
|
"google-timezones-json": "^1.0.2",
|
||||||
"inquirer": "^7.0.1",
|
"inquirer": "^7.0.1",
|
||||||
|
"json-diff": "^0.5.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"jwks-rsa": "~1.12.1",
|
"jwks-rsa": "~1.12.1",
|
||||||
"localtunnel": "^2.0.0",
|
"localtunnel": "^2.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mysql2": "~2.2.0",
|
"mysql2": "~2.2.0",
|
||||||
"n8n-core": "~0.75.0",
|
"n8n-core": "~0.77.0",
|
||||||
"n8n-editor-ui": "~0.96.0",
|
"n8n-editor-ui": "~0.98.0",
|
||||||
"n8n-nodes-base": "~0.123.0",
|
"n8n-nodes-base": "~0.126.0",
|
||||||
"n8n-workflow": "~0.62.0",
|
"n8n-workflow": "~0.63.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"open": "^7.0.0",
|
"open": "^7.0.0",
|
||||||
"pg": "^8.3.0",
|
"pg": "^8.3.0",
|
||||||
|
|
|
@ -188,6 +188,7 @@ export interface IExecutionsListResponse {
|
||||||
count: number;
|
count: number;
|
||||||
// results: IExecutionShortResponse[];
|
// results: IExecutionShortResponse[];
|
||||||
results: IExecutionsSummary[];
|
results: IExecutionsSummary[];
|
||||||
|
estimated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionsStopData {
|
export interface IExecutionsStopData {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
CredentialsHelper,
|
CredentialsHelper,
|
||||||
CredentialsOverwrites,
|
CredentialsOverwrites,
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
|
DatabaseType,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
GenericHelpers,
|
GenericHelpers,
|
||||||
|
@ -88,6 +89,7 @@ import {
|
||||||
IRunData,
|
IRunData,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
|
LoggerProxy,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -1612,8 +1614,7 @@ class App {
|
||||||
executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]);
|
executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]);
|
||||||
|
|
||||||
const countFilter = JSON.parse(JSON.stringify(filter));
|
const countFilter = JSON.parse(JSON.stringify(filter));
|
||||||
countFilter.select = ['id'];
|
countFilter.id = Not(In(executingWorkflowIds));
|
||||||
countFilter.where = {id: Not(In(executingWorkflowIds))};
|
|
||||||
|
|
||||||
const resultsQuery = await Db.collections.Execution!
|
const resultsQuery = await Db.collections.Execution!
|
||||||
.createQueryBuilder("execution")
|
.createQueryBuilder("execution")
|
||||||
|
@ -1645,10 +1646,10 @@ class App {
|
||||||
|
|
||||||
const resultsPromise = resultsQuery.getMany();
|
const resultsPromise = resultsQuery.getMany();
|
||||||
|
|
||||||
const countPromise = Db.collections.Execution!.count(countFilter);
|
const countPromise = getExecutionsCount(countFilter);
|
||||||
|
|
||||||
const results: IExecutionFlattedDb[] = await resultsPromise;
|
const results: IExecutionFlattedDb[] = await resultsPromise;
|
||||||
const count = await countPromise;
|
const countedObjects = await countPromise;
|
||||||
|
|
||||||
const returnResults: IExecutionsSummary[] = [];
|
const returnResults: IExecutionsSummary[] = [];
|
||||||
|
|
||||||
|
@ -1667,8 +1668,9 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count,
|
count: countedObjects.count,
|
||||||
results: returnResults,
|
results: returnResults,
|
||||||
|
estimated: countedObjects.estimate,
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -2161,3 +2163,35 @@ export async function start(): Promise<void> {
|
||||||
await app.externalHooks.run('n8n.ready', [app]);
|
await app.externalHooks.run('n8n.ready', [app]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: number; estimate: boolean; }> {
|
||||||
|
|
||||||
|
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
|
||||||
|
const filteredFields = Object.keys(countFilter).filter(field => field !== 'id');
|
||||||
|
|
||||||
|
// Do regular count for other databases than pgsql and
|
||||||
|
// if we are filtering based on workflowId or finished fields.
|
||||||
|
if (dbType !== 'postgresdb' || filteredFields.length > 0) {
|
||||||
|
const count = await Db.collections.Execution!.count(countFilter);
|
||||||
|
return { count, estimate: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get an estimate of rows count.
|
||||||
|
const estimateRowsNumberSql = "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';";
|
||||||
|
const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(estimateRowsNumberSql);
|
||||||
|
|
||||||
|
const estimate = parseInt(rows[0].n_live_tup, 10);
|
||||||
|
// If over 100k, return just an estimate.
|
||||||
|
if (estimate > 100000) {
|
||||||
|
// if less than 100k, we get the real count as even a full
|
||||||
|
// table scan should not take so long.
|
||||||
|
return { count: estimate, estimate: true };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
LoggerProxy.warn('Unable to get executions count from postgres: ' + err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await Db.collections.Execution!.count(countFilter);
|
||||||
|
return { count, estimate: false };
|
||||||
|
}
|
||||||
|
|
|
@ -608,13 +608,12 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
executionId = parentExecutionId !== undefined ? parentExecutionId : await ActiveExecutions.getInstance().add(runData);
|
executionId = parentExecutionId !== undefined ? parentExecutionId : await ActiveExecutions.getInstance().add(runData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const runExecutionData = runData.executionData as IRunExecutionData;
|
let data;
|
||||||
|
try {
|
||||||
// Get the needed credentials for the current workflow as they will differ to the ones of the
|
// Get the needed credentials for the current workflow as they will differ to the ones of the
|
||||||
// calling workflow.
|
// calling workflow.
|
||||||
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
const credentials = await WorkflowCredentials(workflowData!.nodes);
|
||||||
|
|
||||||
|
|
||||||
// Create new additionalData to have different workflow loaded and to call
|
// Create new additionalData to have different workflow loaded and to call
|
||||||
// different webooks
|
// different webooks
|
||||||
const additionalDataIntegrated = await getBase(credentials);
|
const additionalDataIntegrated = await getBase(credentials);
|
||||||
|
@ -634,6 +633,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
|
|
||||||
additionalDataIntegrated.executionTimeoutTimestamp = subworkflowTimeout;
|
additionalDataIntegrated.executionTimeoutTimestamp = subworkflowTimeout;
|
||||||
|
|
||||||
|
const runExecutionData = runData.executionData as IRunExecutionData;
|
||||||
|
|
||||||
// Execute the workflow
|
// Execute the workflow
|
||||||
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData);
|
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData);
|
||||||
|
@ -645,7 +645,41 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
workflowExecute,
|
workflowExecute,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const data = await workflowExecute.processRunExecutionData(workflow);
|
data = await workflowExecute.processRunExecutionData(workflow);
|
||||||
|
} catch (error) {
|
||||||
|
const fullRunData: IRun = {
|
||||||
|
data: {
|
||||||
|
resultData: {
|
||||||
|
error,
|
||||||
|
runData: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
finished: false,
|
||||||
|
mode: 'integrated',
|
||||||
|
startedAt: new Date(),
|
||||||
|
stoppedAt: new Date(),
|
||||||
|
};
|
||||||
|
// When failing, we might not have finished the execution
|
||||||
|
// Therefore, database might not contain finished errors.
|
||||||
|
// Force an update to db as there should be no harm doing this
|
||||||
|
|
||||||
|
const fullExecutionData: IExecutionDb = {
|
||||||
|
data: fullRunData.data,
|
||||||
|
mode: fullRunData.mode,
|
||||||
|
finished: fullRunData.finished ? fullRunData.finished : false,
|
||||||
|
startedAt: fullRunData.startedAt,
|
||||||
|
stoppedAt: fullRunData.stoppedAt,
|
||||||
|
workflowData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
|
await Db.collections.Execution!.update(executionId, executionData as IExecutionFlattedDb);
|
||||||
|
throw {
|
||||||
|
...error,
|
||||||
|
stack: error!.stack,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
IBullJobResponse,
|
IBullJobResponse,
|
||||||
ICredentialsOverwrite,
|
ICredentialsOverwrite,
|
||||||
ICredentialsTypeData,
|
ICredentialsTypeData,
|
||||||
|
IExecutionDb,
|
||||||
IExecutionFlattedDb,
|
IExecutionFlattedDb,
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IProcessMessageDataHook,
|
IProcessMessageDataHook,
|
||||||
|
@ -29,6 +30,7 @@ import {
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IRun,
|
IRun,
|
||||||
|
IWorkflowBase,
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -85,11 +87,15 @@ export class WorkflowRunner {
|
||||||
* @param {string} executionId
|
* @param {string} executionId
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) {
|
async processError(error: ExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string, hooks?: WorkflowHooks) {
|
||||||
const fullRunData: IRun = {
|
const fullRunData: IRun = {
|
||||||
data: {
|
data: {
|
||||||
resultData: {
|
resultData: {
|
||||||
error,
|
error: {
|
||||||
|
...error,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
},
|
||||||
runData: {},
|
runData: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -102,6 +108,10 @@ export class WorkflowRunner {
|
||||||
// Remove from active execution with empty data. That will
|
// Remove from active execution with empty data. That will
|
||||||
// set the execution to failed.
|
// set the execution to failed.
|
||||||
this.activeExecutions.remove(executionId, fullRunData);
|
this.activeExecutions.remove(executionId, fullRunData);
|
||||||
|
|
||||||
|
if (hooks) {
|
||||||
|
await hooks.executeHookFunctions('workflowExecuteAfter', [fullRunData]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -159,7 +169,6 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
|
||||||
|
|
||||||
// Soft timeout to stop workflow execution after current running node
|
// Soft timeout to stop workflow execution after current running node
|
||||||
// Changes were made by adding the `workflowTimeout` to the `additionalData`
|
// Changes were made by adding the `workflowTimeout` to the `additionalData`
|
||||||
// So that the timeout will also work for executions with nested workflows.
|
// So that the timeout will also work for executions with nested workflows.
|
||||||
|
@ -178,13 +187,13 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Register the active execution
|
// Register the active execution
|
||||||
const executionId = await this.activeExecutions.add(data, undefined);
|
const executionId = await this.activeExecutions.add(data, undefined);
|
||||||
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, {executionId});
|
let workflowExecution: PCancelable<IRun>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, { executionId });
|
||||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
|
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
|
||||||
|
|
||||||
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({sessionId: data.sessionId});
|
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({sessionId: data.sessionId});
|
||||||
|
|
||||||
let workflowExecution: PCancelable<IRun>;
|
|
||||||
if (data.executionData !== undefined) {
|
if (data.executionData !== undefined) {
|
||||||
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId});
|
Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId});
|
||||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData);
|
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData);
|
||||||
|
@ -218,8 +227,16 @@ export class WorkflowRunner {
|
||||||
fullRunData.finished = false;
|
fullRunData.finished = false;
|
||||||
}
|
}
|
||||||
this.activeExecutions.remove(executionId, fullRunData);
|
this.activeExecutions.remove(executionId, fullRunData);
|
||||||
|
}).catch((error) => {
|
||||||
|
this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await this.processError(error, new Date(), data.executionMode, executionId, additionalData.hooks);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
return executionId;
|
return executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,39 +264,48 @@ export class WorkflowRunner {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
};
|
};
|
||||||
const job = await this.jobQueue.add(jobData, jobOptions);
|
let job: Bull.Job;
|
||||||
|
let hooks: WorkflowHooks;
|
||||||
|
try {
|
||||||
|
job = await this.jobQueue.add(jobData, jobOptions);
|
||||||
|
|
||||||
console.log('Started with ID: ' + job.id.toString());
|
console.log('Started with ID: ' + job.id.toString());
|
||||||
|
|
||||||
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
||||||
|
|
||||||
// Normally also workflow should be supplied here but as it only used for sending
|
// Normally also workflow should be supplied here but as it only used for sending
|
||||||
// data to editor-UI is not needed.
|
// data to editor-UI is not needed.
|
||||||
hooks.executeHookFunctions('workflowExecuteBefore', []);
|
hooks.executeHookFunctions('workflowExecuteBefore', []);
|
||||||
|
} catch (error) {
|
||||||
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
|
// "workflowExecuteAfter" which we require.
|
||||||
|
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
||||||
|
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const workflowExecution: PCancelable<IRun> = new PCancelable(async (resolve, reject, onCancel) => {
|
const workflowExecution: PCancelable<IRun> = new PCancelable(async (resolve, reject, onCancel) => {
|
||||||
onCancel.shouldReject = false;
|
onCancel.shouldReject = false;
|
||||||
onCancel(async () => {
|
onCancel(async () => {
|
||||||
await Queue.getInstance().stopJob(job);
|
await Queue.getInstance().stopJob(job);
|
||||||
|
|
||||||
const fullRunData :IRun = {
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
data: {
|
// "workflowExecuteAfter" which we require.
|
||||||
resultData: {
|
const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
||||||
error: new WorkflowOperationError('Workflow has been canceled!'),
|
|
||||||
runData: {},
|
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
},
|
await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);
|
||||||
},
|
|
||||||
mode: data.executionMode,
|
reject(error);
|
||||||
startedAt: new Date(),
|
|
||||||
stoppedAt: new Date(),
|
|
||||||
};
|
|
||||||
this.activeExecutions.remove(executionId, fullRunData);
|
|
||||||
resolve(fullRunData);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobData: Promise<IBullJobResponse> = job.finished();
|
const jobData: Promise<IBullJobResponse> = job.finished();
|
||||||
|
|
||||||
const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number;
|
const queueRecoveryInterval = config.get('queue.bull.queueRecoveryInterval') as number;
|
||||||
|
|
||||||
|
const racingPromises: Array<Promise<IBullJobResponse | object>> = [jobData];
|
||||||
|
|
||||||
|
let clearWatchdogInterval;
|
||||||
if (queueRecoveryInterval > 0) {
|
if (queueRecoveryInterval > 0) {
|
||||||
/*************************************************
|
/*************************************************
|
||||||
* Long explanation about what this solves: *
|
* Long explanation about what this solves: *
|
||||||
|
@ -295,7 +321,7 @@ export class WorkflowRunner {
|
||||||
*************************************************/
|
*************************************************/
|
||||||
let watchDogInterval: NodeJS.Timeout | undefined;
|
let watchDogInterval: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
const watchDog = new Promise((res) => {
|
const watchDog: Promise<object> = new Promise((res) => {
|
||||||
watchDogInterval = setInterval(async () => {
|
watchDogInterval = setInterval(async () => {
|
||||||
const currentJob = await this.jobQueue.getJob(job.id);
|
const currentJob = await this.jobQueue.getJob(job.id);
|
||||||
// When null means job is finished (not found in queue)
|
// When null means job is finished (not found in queue)
|
||||||
|
@ -306,22 +332,33 @@ export class WorkflowRunner {
|
||||||
}, queueRecoveryInterval * 1000);
|
}, queueRecoveryInterval * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
racingPromises.push(watchDog);
|
||||||
|
|
||||||
const clearWatchdogInterval = () => {
|
clearWatchdogInterval = () => {
|
||||||
if (watchDogInterval) {
|
if (watchDogInterval) {
|
||||||
clearInterval(watchDogInterval);
|
clearInterval(watchDogInterval);
|
||||||
watchDogInterval = undefined;
|
watchDogInterval = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.race([jobData, watchDog]);
|
|
||||||
clearWatchdogInterval();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
await jobData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race(racingPromises);
|
||||||
|
if (clearWatchdogInterval !== undefined) {
|
||||||
|
clearWatchdogInterval();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||||
|
// "workflowExecuteAfter" which we require.
|
||||||
|
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(data.executionMode, executionId, data.workflowData, { retryOf: data.retryOf ? data.retryOf.toString() : undefined });
|
||||||
|
Logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`);
|
||||||
|
if (clearWatchdogInterval !== undefined) {
|
||||||
|
clearWatchdogInterval();
|
||||||
|
}
|
||||||
|
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb;
|
const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb;
|
||||||
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
|
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse;
|
||||||
|
@ -333,7 +370,6 @@ export class WorkflowRunner {
|
||||||
stoppedAt: fullExecutionData.stoppedAt,
|
stoppedAt: fullExecutionData.stoppedAt,
|
||||||
} as IRun;
|
} as IRun;
|
||||||
|
|
||||||
|
|
||||||
this.activeExecutions.remove(executionId, runData);
|
this.activeExecutions.remove(executionId, runData);
|
||||||
// Normally also static data should be supplied here but as it only used for sending
|
// Normally also static data should be supplied here but as it only used for sending
|
||||||
// data to editor-UI is not needed.
|
// data to editor-UI is not needed.
|
||||||
|
@ -427,8 +463,13 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
|
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
|
||||||
|
|
||||||
|
try {
|
||||||
// Send all data to subprocess it needs to run the workflow
|
// Send all data to subprocess it needs to run the workflow
|
||||||
subprocess.send({ type: 'startWorkflow', data } as IProcessMessage);
|
subprocess.send({ type: 'startWorkflow', data } as IProcessMessage);
|
||||||
|
} catch (error) {
|
||||||
|
await this.processError(error, new Date(), data.executionMode, executionId, workflowHooks);
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
// Start timeout for the execution
|
// Start timeout for the execution
|
||||||
let executionTimeout: NodeJS.Timeout;
|
let executionTimeout: NodeJS.Timeout;
|
||||||
|
@ -476,14 +517,14 @@ export class WorkflowRunner {
|
||||||
} else if (message.type === 'processError') {
|
} else if (message.type === 'processError') {
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
const executionError = message.data.executionError as ExecutionError;
|
const executionError = message.data.executionError as ExecutionError;
|
||||||
this.processError(executionError, startedAt, data.executionMode, executionId);
|
await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks);
|
||||||
|
|
||||||
} else if (message.type === 'processHook') {
|
} else if (message.type === 'processHook') {
|
||||||
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
|
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
|
||||||
} else if (message.type === 'timeout') {
|
} else if (message.type === 'timeout') {
|
||||||
// Execution timed out and its process has been terminated
|
// Execution timed out and its process has been terminated
|
||||||
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
||||||
|
|
||||||
|
// No need to add hook here as the subprocess takes care of calling the hooks
|
||||||
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
||||||
} else if (message.type === 'startExecution') {
|
} else if (message.type === 'startExecution') {
|
||||||
const executionId = await this.activeExecutions.add(message.data.runData);
|
const executionId = await this.activeExecutions.add(message.data.runData);
|
||||||
|
@ -506,13 +547,13 @@ export class WorkflowRunner {
|
||||||
// Execution timed out and its process has been terminated
|
// Execution timed out and its process has been terminated
|
||||||
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
||||||
|
|
||||||
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
await this.processError(timeoutError, startedAt, data.executionMode, executionId, workflowHooks);
|
||||||
} else if (code !== 0) {
|
} else if (code !== 0) {
|
||||||
Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId});
|
Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId});
|
||||||
// Process did exit with error code, so something went wrong.
|
// Process did exit with error code, so something went wrong.
|
||||||
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
|
const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!');
|
||||||
|
|
||||||
this.processError(executionError, startedAt, data.executionMode, executionId);
|
await this.processError(executionError, startedAt, data.executionMode, executionId, workflowHooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const executionId of childExecutionIds) {
|
for(const executionId of childExecutionIds) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
IWorkflowExecuteHooks,
|
IWorkflowExecuteHooks,
|
||||||
LoggerProxy,
|
LoggerProxy,
|
||||||
Workflow,
|
Workflow,
|
||||||
|
WorkflowExecuteMode,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -315,7 +316,7 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
for (const executionId of executionIds) {
|
for (const executionId of executionIds) {
|
||||||
const childWorkflowExecute = workflowRunner.childExecutions[executionId];
|
const childWorkflowExecute = workflowRunner.childExecutions[executionId];
|
||||||
runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt);
|
runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt);
|
||||||
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : undefined;
|
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
|
|
||||||
// If there is any data send it to parent process, if execution timedout add the error
|
// If there is any data send it to parent process, if execution timedout add the error
|
||||||
await childWorkflowExecute.workflowExecute.processSuccessExecution(workflowRunner.childExecutions[executionId].startedAt, childWorkflowExecute.workflow, timeOutError);
|
await childWorkflowExecute.workflowExecute.processSuccessExecution(workflowRunner.childExecutions[executionId].startedAt, childWorkflowExecute.workflow, timeOutError);
|
||||||
|
@ -324,7 +325,7 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
// Workflow started already executing
|
// Workflow started already executing
|
||||||
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
|
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
|
||||||
|
|
||||||
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : undefined;
|
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
|
|
||||||
// If there is any data send it to parent process, if execution timedout add the error
|
// If there is any data send it to parent process, if execution timedout add the error
|
||||||
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError);
|
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError);
|
||||||
|
@ -336,8 +337,8 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
runData: {},
|
runData: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
finished: message.type !== 'timeout',
|
finished: false,
|
||||||
mode: workflowRunner.data!.executionMode,
|
mode: workflowRunner.data ? workflowRunner.data!.executionMode : 'own' as WorkflowExecuteMode,
|
||||||
startedAt: workflowRunner.startedAt,
|
startedAt: workflowRunner.startedAt,
|
||||||
stoppedAt: new Date(),
|
stoppedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2017"
|
"es2017",
|
||||||
|
"ES2020.Promise"
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "0.75.0",
|
"version": "0.77.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
"file-type": "^14.6.2",
|
"file-type": "^14.6.2",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"n8n-workflow": "~0.62.0",
|
"n8n-workflow": "~0.63.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
|
|
@ -807,7 +807,7 @@ export class WorkflowExecute {
|
||||||
})()
|
})()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
if (gotCancel && executionError === undefined) {
|
if (gotCancel && executionError === undefined) {
|
||||||
return this.processSuccessExecution(startedAt, workflow, new WorkflowOperationError('Workflow has been canceled!'));
|
return this.processSuccessExecution(startedAt, workflow, new WorkflowOperationError('Workflow has been canceled or timed out!'));
|
||||||
}
|
}
|
||||||
return this.processSuccessExecution(startedAt, workflow, executionError);
|
return this.processSuccessExecution(startedAt, workflow, executionError);
|
||||||
})
|
})
|
||||||
|
@ -844,7 +844,11 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
if (executionError !== undefined) {
|
if (executionError !== undefined) {
|
||||||
Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id });
|
Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id });
|
||||||
fullRunData.data.resultData.error = executionError;
|
fullRunData.data.resultData.error = {
|
||||||
|
...executionError,
|
||||||
|
message: executionError.message,
|
||||||
|
stack: executionError.stack,
|
||||||
|
} as ExecutionError;
|
||||||
} else {
|
} else {
|
||||||
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
|
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
|
||||||
fullRunData.finished = true;
|
fullRunData.finished = true;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.96.0",
|
"version": "0.98.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.set": "^4.3.2",
|
"lodash.set": "^4.3.2",
|
||||||
"n8n-workflow": "~0.62.0",
|
"n8n-workflow": "~0.63.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"normalize-wheel": "^1.0.1",
|
"normalize-wheel": "^1.0.1",
|
||||||
"prismjs": "^1.17.1",
|
"prismjs": "^1.17.1",
|
||||||
|
|
|
@ -202,7 +202,7 @@ export interface IVariableSelectorOption {
|
||||||
|
|
||||||
// Simple version of n8n-workflow.Workflow
|
// Simple version of n8n-workflow.Workflow
|
||||||
export interface IWorkflowData {
|
export interface IWorkflowData {
|
||||||
id?: string;
|
id?: string | number;
|
||||||
name?: string;
|
name?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
nodes: INode[];
|
nodes: INode[];
|
||||||
|
@ -212,7 +212,7 @@ export interface IWorkflowData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowDataUpdate {
|
export interface IWorkflowDataUpdate {
|
||||||
id?: string;
|
id?: string | number;
|
||||||
name?: string;
|
name?: string;
|
||||||
nodes?: INode[];
|
nodes?: INode[];
|
||||||
connections?: IConnections;
|
connections?: IConnections;
|
||||||
|
@ -325,6 +325,7 @@ export interface IExecutionShortResponse {
|
||||||
export interface IExecutionsListResponse {
|
export interface IExecutionsListResponse {
|
||||||
count: number;
|
count: number;
|
||||||
results: IExecutionsSummary[];
|
results: IExecutionsSummary[];
|
||||||
|
estimated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionsCurrentSummaryExtended {
|
export interface IExecutionsCurrentSummaryExtended {
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
<div v-if="!binaryData">
|
<div v-if="!binaryData">
|
||||||
Data to display did not get found
|
Data to display did not get found
|
||||||
</div>
|
</div>
|
||||||
|
<video v-else-if="binaryData.mimeType && binaryData.mimeType.startsWith('video/')" controls autoplay>
|
||||||
|
<source :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" :type="binaryData.mimeType">
|
||||||
|
Your browser does not support the video element. Kindly update it to latest version.
|
||||||
|
</video>
|
||||||
<embed v-else :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
|
<embed v-else :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Workflow Executions (${combinedExecutions.length}/${combinedExecutionsCount})`" :before-close="closeDialog">
|
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Workflow Executions ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :before-close="closeDialog">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="4" class="filter-headline">
|
<el-col :span="4" class="filter-headline">
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
<div class="selection-options">
|
<div class="selection-options">
|
||||||
<span v-if="checkAll === true || isIndeterminate === true">
|
<span v-if="checkAll === true || isIndeterminate === true">
|
||||||
Selected: {{numSelected}}/{{finishedExecutionsCount}}
|
Selected: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}}
|
||||||
<el-button type="danger" title="Delete Selected" icon="el-icon-delete" size="mini" @click="handleDeleteSelected" circle></el-button>
|
<el-button type="danger" title="Delete Selected" icon="el-icon-delete" size="mini" @click="handleDeleteSelected" circle></el-button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,7 +142,7 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length">
|
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length || finishedExecutionsCountEstimated === true">
|
||||||
<el-button title="Load More" @click="loadMore()" size="small" :disabled="isDataLoading">
|
<el-button title="Load More" @click="loadMore()" size="small" :disabled="isDataLoading">
|
||||||
<font-awesome-icon icon="sync" /> Load More
|
<font-awesome-icon icon="sync" /> Load More
|
||||||
</el-button>
|
</el-button>
|
||||||
|
@ -200,6 +200,7 @@ export default mixins(
|
||||||
return {
|
return {
|
||||||
finishedExecutions: [] as IExecutionsSummary[],
|
finishedExecutions: [] as IExecutionsSummary[],
|
||||||
finishedExecutionsCount: 0,
|
finishedExecutionsCount: 0,
|
||||||
|
finishedExecutionsCountEstimated: false,
|
||||||
|
|
||||||
checkAll: false,
|
checkAll: false,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
|
@ -256,7 +257,7 @@ export default mixins(
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
combinedExecutionsCount (): number {
|
combinedExecutionsCount (): number {
|
||||||
return this.activeExecutions.length + this.finishedExecutionsCount;
|
return 0 + this.activeExecutions.length + this.finishedExecutionsCount;
|
||||||
},
|
},
|
||||||
numSelected (): number {
|
numSelected (): number {
|
||||||
if (this.checkAll === true) {
|
if (this.checkAll === true) {
|
||||||
|
@ -489,16 +490,19 @@ export default mixins(
|
||||||
}
|
}
|
||||||
this.finishedExecutions = this.finishedExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10));
|
this.finishedExecutions = this.finishedExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10));
|
||||||
this.finishedExecutionsCount = results[0].count;
|
this.finishedExecutionsCount = results[0].count;
|
||||||
|
this.finishedExecutionsCountEstimated = results[0].estimated;
|
||||||
},
|
},
|
||||||
async loadFinishedExecutions (): Promise<void> {
|
async loadFinishedExecutions (): Promise<void> {
|
||||||
if (this.filter.status === 'running') {
|
if (this.filter.status === 'running') {
|
||||||
this.finishedExecutions = [];
|
this.finishedExecutions = [];
|
||||||
this.finishedExecutionsCount = 0;
|
this.finishedExecutionsCount = 0;
|
||||||
|
this.finishedExecutionsCountEstimated = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await this.restApi().getPastExecutions(this.workflowFilterPast, this.requestItemsPerRequest);
|
const data = await this.restApi().getPastExecutions(this.workflowFilterPast, this.requestItemsPerRequest);
|
||||||
this.finishedExecutions = data.results;
|
this.finishedExecutions = data.results;
|
||||||
this.finishedExecutionsCount = data.count;
|
this.finishedExecutionsCount = data.count;
|
||||||
|
this.finishedExecutionsCountEstimated = data.estimated;
|
||||||
},
|
},
|
||||||
async loadMore () {
|
async loadMore () {
|
||||||
if (this.filter.status === 'running') {
|
if (this.filter.status === 'running') {
|
||||||
|
@ -526,6 +530,7 @@ export default mixins(
|
||||||
|
|
||||||
this.finishedExecutions.push.apply(this.finishedExecutions, data.results);
|
this.finishedExecutions.push.apply(this.finishedExecutions, data.results);
|
||||||
this.finishedExecutionsCount = data.count;
|
this.finishedExecutionsCount = data.count;
|
||||||
|
this.finishedExecutionsCountEstimated = data.estimated;
|
||||||
|
|
||||||
this.isDataLoading = false;
|
this.isDataLoading = false;
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
@keydown.esc="onEscape"
|
@keydown.esc="onEscape"
|
||||||
ref="input"
|
ref="input"
|
||||||
size="4"
|
size="4"
|
||||||
v-click-outside="onBlur"
|
v-click-outside="onClickOutside"
|
||||||
/>
|
/>
|
||||||
</ExpandableInputBase>
|
</ExpandableInputBase>
|
||||||
</template>
|
</template>
|
||||||
|
@ -47,8 +47,10 @@ export default Vue.extend({
|
||||||
onEnter() {
|
onEnter() {
|
||||||
this.$emit('enter', (this.$refs.input as HTMLInputElement).value);
|
this.$emit('enter', (this.$refs.input as HTMLInputElement).value);
|
||||||
},
|
},
|
||||||
onBlur() {
|
onClickOutside(e: Event) {
|
||||||
|
if (e.type === 'click') {
|
||||||
this.$emit('blur', (this.$refs.input as HTMLInputElement).value);
|
this.$emit('blur', (this.$refs.input as HTMLInputElement).value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onEscape() {
|
onEscape() {
|
||||||
this.$emit('esc');
|
this.$emit('esc');
|
||||||
|
|
|
@ -408,6 +408,9 @@ export default mixins(
|
||||||
const workflowData = await this.getWorkflowDataToSave();
|
const workflowData = await this.getWorkflowDataToSave();
|
||||||
|
|
||||||
const {tags, ...data} = workflowData;
|
const {tags, ...data} = workflowData;
|
||||||
|
if (data.id && typeof data.id === 'string') {
|
||||||
|
data.id = parseInt(data.id, 10);
|
||||||
|
}
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||||
type: 'application/json;charset=utf-8',
|
type: 'application/json;charset=utf-8',
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@before-leave="beforeLeave"
|
@before-leave="beforeLeave"
|
||||||
@leave="leave"
|
@leave="leave"
|
||||||
>
|
>
|
||||||
<div v-for="(item, index) in elements" :key="item.key" :class="item.type">
|
<div v-for="(item, index) in elements" :key="item.key" :class="item.type" :data-key="item.key">
|
||||||
<CreatorItem
|
<CreatorItem
|
||||||
:item="item"
|
:item="item"
|
||||||
:active="activeIndex === index && !disabled"
|
:active="activeIndex === index && !disabled"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SlideTransition>
|
<SlideTransition>
|
||||||
<div class="node-creator" v-if="active" v-click-outside="closeCreator">
|
<div class="node-creator" v-if="active" v-click-outside="onClickOutside">
|
||||||
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
|
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
|
||||||
</div>
|
</div>
|
||||||
</SlideTransition>
|
</SlideTransition>
|
||||||
|
@ -29,9 +29,17 @@ export default Vue.extend({
|
||||||
props: [
|
props: [
|
||||||
'active',
|
'active',
|
||||||
],
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allNodeTypes: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
nodeTypes(): INodeTypeDescription[] {
|
||||||
|
return this.$store.getters.allNodeTypes;
|
||||||
|
},
|
||||||
visibleNodeTypes(): INodeTypeDescription[] {
|
visibleNodeTypes(): INodeTypeDescription[] {
|
||||||
return this.$store.getters.allNodeTypes
|
return this.allNodeTypes
|
||||||
.filter((nodeType: INodeTypeDescription) => {
|
.filter((nodeType: INodeTypeDescription) => {
|
||||||
return !HIDDEN_NODES.includes(nodeType.name);
|
return !HIDDEN_NODES.includes(nodeType.name);
|
||||||
});
|
});
|
||||||
|
@ -64,13 +72,22 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeCreator () {
|
onClickOutside (e: Event) {
|
||||||
|
if (e.type === 'click') {
|
||||||
this.$emit('closeNodeCreator');
|
this.$emit('closeNodeCreator');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
nodeTypeSelected (nodeTypeName: string) {
|
nodeTypeSelected (nodeTypeName: string) {
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
nodeTypes(newList, prevList) {
|
||||||
|
if (prevList.length === 0) {
|
||||||
|
this.allNodeTypes = newList;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{'tags-container': true, focused}" @keydown.stop v-click-outside="onBlur">
|
<div :class="{'tags-container': true, focused}" @keydown.stop v-click-outside="onClickOutside">
|
||||||
<el-select
|
<el-select
|
||||||
:popperAppendToBody="false"
|
:popperAppendToBody="false"
|
||||||
:value="appliedTags"
|
:value="appliedTags"
|
||||||
|
@ -215,8 +215,10 @@ export default mixins(showMessage).extend({
|
||||||
this.focusOnInput();
|
this.focusOnInput();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onBlur() {
|
onClickOutside(e: Event) {
|
||||||
|
if (e.type === 'click') {
|
||||||
this.$emit('blur');
|
this.$emit('blur');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<div :key="scope.row.id">
|
<div :key="scope.row.id">
|
||||||
<span class="name">{{scope.row.name}}</span>
|
<span class="name">{{scope.row.name}}</span>
|
||||||
<TagsContainer class="hidden-sm-and-down" :tagIds="getIds(scope.row.tags)" :limit="3" />
|
<TagsContainer class="hidden-sm-and-down" :tagIds="getIds(scope.row.tags)" :limit="3" @click="onTagClick" :clickable="true" :hoverable="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
@ -124,6 +124,11 @@ export default mixins(
|
||||||
updateTagsFilter(tags: string[]) {
|
updateTagsFilter(tags: string[]) {
|
||||||
this.filterTagIds = tags;
|
this.filterTagIds = tags;
|
||||||
},
|
},
|
||||||
|
onTagClick(tagId: string) {
|
||||||
|
if (tagId !== 'count' && !this.filterTagIds.includes(tagId)) {
|
||||||
|
this.filterTagIds.push(tagId);
|
||||||
|
}
|
||||||
|
},
|
||||||
async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any
|
async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any
|
||||||
if (column.label !== 'Active') {
|
if (column.label !== 'Active') {
|
||||||
|
|
||||||
|
|
|
@ -213,33 +213,16 @@ export const pushConnection = mixins(
|
||||||
|
|
||||||
const runDataExecuted = pushData.data;
|
const runDataExecuted = pushData.data;
|
||||||
|
|
||||||
let runDataExecutedErrorMessage;
|
const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data.resultData.error);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const workflow = this.getWorkflow();
|
const workflow = this.getWorkflow();
|
||||||
if (runDataExecuted.finished !== true) {
|
if (runDataExecuted.finished !== true) {
|
||||||
// There was a problem with executing the workflow
|
|
||||||
let errorMessage = 'There was a problem executing the workflow!';
|
|
||||||
|
|
||||||
if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) {
|
|
||||||
let nodeName: string | undefined;
|
|
||||||
if (runDataExecuted.data.resultData.error.node) {
|
|
||||||
nodeName = typeof runDataExecuted.data.resultData.error.node === 'string'
|
|
||||||
? runDataExecuted.data.resultData.error.node
|
|
||||||
: runDataExecuted.data.resultData.error.node.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedError = nodeName
|
|
||||||
? `${nodeName}: ${runDataExecuted.data.resultData.error.message}`
|
|
||||||
: runDataExecuted.data.resultData.error.message;
|
|
||||||
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
runDataExecutedErrorMessage = errorMessage;
|
|
||||||
|
|
||||||
this.$titleSet(workflow.name as string, 'ERROR');
|
this.$titleSet(workflow.name as string, 'ERROR');
|
||||||
|
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title: 'Problem executing workflow',
|
title: 'Problem executing workflow',
|
||||||
message: errorMessage,
|
message: runDataExecutedErrorMessage,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ElNotificationOptions } from 'element-ui/types/notification';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
import { ExecutionError } from 'n8n-workflow';
|
||||||
|
|
||||||
export const showMessage = mixins(externalHooks).extend({
|
export const showMessage = mixins(externalHooks).extend({
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -15,6 +16,27 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
return Notification(messageData);
|
return Notification(messageData);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
$getExecutionError(error?: ExecutionError) {
|
||||||
|
// There was a problem with executing the workflow
|
||||||
|
let errorMessage = 'There was a problem executing the workflow!';
|
||||||
|
|
||||||
|
if (error && error.message) {
|
||||||
|
let nodeName: string | undefined;
|
||||||
|
if (error.node) {
|
||||||
|
nodeName = typeof error.node === 'string'
|
||||||
|
? error.node
|
||||||
|
: error.node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedError = nodeName
|
||||||
|
? `${nodeName}: ${error.message}`
|
||||||
|
: error.message;
|
||||||
|
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessage;
|
||||||
|
},
|
||||||
|
|
||||||
$showError(error: Error, title: string, message?: string) {
|
$showError(error: Error, title: string, message?: string) {
|
||||||
const messageLine = message ? `${message}<br/>` : '';
|
const messageLine = message ? `${message}<br/>` : '';
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
|
|
|
@ -31,9 +31,6 @@ const module: Module<IUiState, IRootState> = {
|
||||||
isModalActive: (state: IUiState) => {
|
isModalActive: (state: IUiState) => {
|
||||||
return (name: string) => state.modalStack.length > 0 && name === state.modalStack[0];
|
return (name: string) => state.modalStack.length > 0 && name === state.modalStack[0];
|
||||||
},
|
},
|
||||||
anyModalsOpen: (state: IUiState) => {
|
|
||||||
return state.modalStack.length > 0;
|
|
||||||
},
|
|
||||||
sidebarMenuCollapsed: (state: IUiState): boolean => state.sidebarMenuCollapsed,
|
sidebarMenuCollapsed: (state: IUiState): boolean => state.sidebarMenuCollapsed,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
|
|
@ -375,6 +375,39 @@ export default mixins(
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
||||||
|
|
||||||
|
if (data.finished !== true && data.data.resultData.error) {
|
||||||
|
// Check if any node contains an error
|
||||||
|
let nodeErrorFound = false;
|
||||||
|
if (data.data.resultData.runData) {
|
||||||
|
const runData = data.data.resultData.runData;
|
||||||
|
errorCheck:
|
||||||
|
for (const nodeName of Object.keys(runData)) {
|
||||||
|
for (const taskData of runData[nodeName]) {
|
||||||
|
if (taskData.error) {
|
||||||
|
nodeErrorFound = true;
|
||||||
|
break errorCheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeErrorFound === false) {
|
||||||
|
const errorMessage = this.$getExecutionError(data.data.resultData.error);
|
||||||
|
this.$showMessage({
|
||||||
|
title: 'Failed execution',
|
||||||
|
message: errorMessage,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.data.resultData.error.stack) {
|
||||||
|
// Display some more information for now in console to make debugging easier
|
||||||
|
// TODO: Improve this in the future by displaying in UI
|
||||||
|
console.error(`Execution ${executionId} error:`); // eslint-disable-line no-console
|
||||||
|
console.error(data.data.resultData.error.stack); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async openWorkflowTemplate (templateId: string) {
|
async openWorkflowTemplate (templateId: string) {
|
||||||
this.setLoadingText('Loading template');
|
this.setLoadingText('Loading template');
|
||||||
|
@ -514,15 +547,29 @@ export default mixins(
|
||||||
// else which should ignore the default keybindings
|
// else which should ignore the default keybindings
|
||||||
for (let index = 0; index < path.length; index++) {
|
for (let index = 0; index < path.length; index++) {
|
||||||
if (path[index].className && typeof path[index].className === 'string' && (
|
if (path[index].className && typeof path[index].className === 'string' && (
|
||||||
path[index].className.includes('el-message-box') ||
|
|
||||||
path[index].className.includes('el-select') ||
|
|
||||||
path[index].className.includes('ignore-key-press')
|
path[index].className.includes('ignore-key-press')
|
||||||
)) {
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const anyModalsOpen = this.$store.getters['ui/anyModalsOpen'];
|
|
||||||
if (anyModalsOpen) {
|
// el-dialog or el-message-box element is open
|
||||||
|
if (window.document.body.classList.contains('el-popup-parent--hidden')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.createNodeActive = false;
|
||||||
|
if (this.activeNode) {
|
||||||
|
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
||||||
|
this.$store.commit('setActiveNode', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// node modal is open
|
||||||
|
if (this.activeNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -533,15 +580,12 @@ export default mixins(
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.callDebounced('deleteSelectedNodes', 500);
|
this.callDebounced('deleteSelectedNodes', 500);
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
|
||||||
this.createNodeActive = false;
|
|
||||||
this.$store.commit('setActiveNode', null);
|
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
this.createNodeActive = !this.createNodeActive && !this.isReadOnly;
|
this.createNodeActive = !this.createNodeActive && !this.isReadOnly;
|
||||||
} else if (e.key === this.controlKeyCode) {
|
} else if (e.key === this.controlKeyCode) {
|
||||||
this.ctrlKeyPressed = true;
|
this.ctrlKeyPressed = true;
|
||||||
} else if (e.key === 'F2') {
|
} else if (e.key === 'F2' && !this.isReadOnly) {
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
if (lastSelectedNode !== null) {
|
if (lastSelectedNode !== null) {
|
||||||
this.callDebounced('renameNodePrompt', 1500, lastSelectedNode.name);
|
this.callDebounced('renameNodePrompt', 1500, lastSelectedNode.name);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-node-dev",
|
"name": "n8n-node-dev",
|
||||||
"version": "0.15.0",
|
"version": "0.17.0",
|
||||||
"description": "CLI to simplify n8n credentials/node development",
|
"description": "CLI to simplify n8n credentials/node development",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -59,8 +59,8 @@
|
||||||
"change-case": "^4.1.1",
|
"change-case": "^4.1.1",
|
||||||
"copyfiles": "^2.1.1",
|
"copyfiles": "^2.1.1",
|
||||||
"inquirer": "^7.0.1",
|
"inquirer": "^7.0.1",
|
||||||
"n8n-core": "~0.75.0",
|
"n8n-core": "~0.77.0",
|
||||||
"n8n-workflow": "~0.62.0",
|
"n8n-workflow": "~0.63.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"replace-in-file": "^6.0.0",
|
"replace-in-file": "^6.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
|
|
@ -3,11 +3,18 @@ import {
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export class TaigaServerApi implements ICredentialType {
|
// https://api.baserow.io/api/redoc/#section/Authentication
|
||||||
name = 'taigaServerApi';
|
|
||||||
displayName = 'Taiga Server API';
|
export class BaserowApi implements ICredentialType {
|
||||||
documentationUrl = 'taiga';
|
name = 'baserowApi';
|
||||||
|
displayName = 'Baserow API';
|
||||||
properties: INodeProperties[] = [
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Host',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string',
|
||||||
|
default: 'https://api.baserow.io',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Username',
|
displayName: 'Username',
|
||||||
name: 'username',
|
name: 'username',
|
||||||
|
@ -19,13 +26,9 @@ export class TaigaServerApi implements ICredentialType {
|
||||||
name: 'password',
|
name: 'password',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
displayName: 'URL',
|
|
||||||
name: 'url',
|
|
||||||
type: 'string',
|
|
||||||
default: '',
|
|
||||||
placeholder: 'https://taiga.yourdomain.com',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
NodePropertyTypes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class HomeAssistantApi implements ICredentialType {
|
||||||
|
name = 'homeAssistantApi';
|
||||||
|
displayName = 'Home Assistant API';
|
||||||
|
documentationUrl = 'homeAssistant';
|
||||||
|
properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Host',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Port',
|
||||||
|
name: 'port',
|
||||||
|
type: 'number' as NodePropertyTypes,
|
||||||
|
default: 8123,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'SSL',
|
||||||
|
name: 'ssl',
|
||||||
|
type: 'boolean' as NodePropertyTypes,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Access Token',
|
||||||
|
name: 'accessToken',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -57,5 +57,12 @@ export class MicrosoftSql implements ICredentialType {
|
||||||
default: 15000,
|
default: 15000,
|
||||||
description: 'Connection timeout in ms.',
|
description: 'Connection timeout in ms.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Request Timeout',
|
||||||
|
name: 'requestTimeout',
|
||||||
|
type: 'number',
|
||||||
|
default: 15000,
|
||||||
|
description: ' Request timeout in ms.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,35 @@ export class SalesforceOAuth2Api implements ICredentialType {
|
||||||
displayName = 'Salesforce OAuth2 API';
|
displayName = 'Salesforce OAuth2 API';
|
||||||
documentationUrl = 'salesforce';
|
documentationUrl = 'salesforce';
|
||||||
properties: INodeProperties[] = [
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Environment Type',
|
||||||
|
name: 'environment',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Production',
|
||||||
|
value: 'production',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sandbox',
|
||||||
|
value: 'sandbox',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'production',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Authorization URL',
|
displayName: 'Authorization URL',
|
||||||
name: 'authUrl',
|
name: 'authUrl',
|
||||||
type: 'hidden',
|
type: 'hidden',
|
||||||
default: 'https://login.salesforce.com/services/oauth2/authorize',
|
|
||||||
required: true,
|
required: true,
|
||||||
|
default: '={{ $self["environment"] === "sandbox" ? "https://test.salesforce.com/services/oauth2/authorize" : "https://login.salesforce.com/services/oauth2/authorize" }}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Access Token URL',
|
displayName: 'Access Token URL',
|
||||||
name: 'accessTokenUrl',
|
name: 'accessTokenUrl',
|
||||||
type: 'string',
|
type: 'hidden',
|
||||||
default: 'https://yourcompany.salesforce.com/services/oauth2/token',
|
|
||||||
required: true,
|
required: true,
|
||||||
|
default: '={{ $self["environment"] === "sandbox" ? "https://test.salesforce.com/services/oauth2/token" : "https://login.salesforce.com/services/oauth2/token" }}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Scope',
|
displayName: 'Scope',
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
NodePropertyTypes,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class ServiceNowOAuth2Api implements ICredentialType {
|
||||||
|
name = 'serviceNowOAuth2Api';
|
||||||
|
extends = [
|
||||||
|
'oAuth2Api',
|
||||||
|
];
|
||||||
|
displayName = 'ServiceNow OAuth2 API';
|
||||||
|
documentationUrl = 'serviceNow';
|
||||||
|
properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Subdomain',
|
||||||
|
name: 'subdomain',
|
||||||
|
type: 'string' as NodePropertyTypes,
|
||||||
|
default: '',
|
||||||
|
placeholder: 'n8n',
|
||||||
|
description: 'The subdomain of your ServiceNow environment',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Authorization URL',
|
||||||
|
name: 'authUrl',
|
||||||
|
type: 'hidden' as NodePropertyTypes,
|
||||||
|
default: '=https://{{$self["subdomain"]}}.service-now.com/oauth_auth.do',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Access Token URL',
|
||||||
|
name: 'accessTokenUrl',
|
||||||
|
type: 'hidden' as NodePropertyTypes,
|
||||||
|
default: '=https://{{$self["subdomain"]}}.service-now.com/oauth_token.do',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Scope',
|
||||||
|
name: 'scope',
|
||||||
|
type: 'hidden' as NodePropertyTypes,
|
||||||
|
default: 'useraccount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Auth URI Query Parameters',
|
||||||
|
name: 'authQueryParameters',
|
||||||
|
type: 'hidden' as NodePropertyTypes,
|
||||||
|
default: 'response_type=code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Auth URI Query Parameters',
|
||||||
|
name: 'authQueryParameters',
|
||||||
|
type: 'hidden' as NodePropertyTypes,
|
||||||
|
default: 'grant_type=authorization_code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Authentication',
|
||||||
|
name: 'authentication',
|
||||||
|
type: 'hidden' as NodePropertyTypes,
|
||||||
|
default: 'header',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import {
|
||||||
|
|
||||||
export class StripeApi implements ICredentialType {
|
export class StripeApi implements ICredentialType {
|
||||||
name = 'stripeApi';
|
name = 'stripeApi';
|
||||||
displayName = 'Stripe Api';
|
displayName = 'Stripe API';
|
||||||
documentationUrl = 'stripe';
|
documentationUrl = 'stripe';
|
||||||
properties: INodeProperties[] = [
|
properties: INodeProperties[] = [
|
||||||
// The credentials to get from user and save encrypted.
|
// The credentials to get from user and save encrypted.
|
||||||
|
|
54
packages/nodes-base/credentials/TaigaApi.credentials.ts
Normal file
54
packages/nodes-base/credentials/TaigaApi.credentials.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class TaigaApi implements ICredentialType {
|
||||||
|
name = 'taigaApi';
|
||||||
|
displayName = 'Taiga API';
|
||||||
|
documentationUrl = 'taiga';
|
||||||
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Username',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Password',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Environment',
|
||||||
|
name: 'environment',
|
||||||
|
type: 'options',
|
||||||
|
default: 'cloud',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Cloud',
|
||||||
|
value: 'cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Self-Hosted',
|
||||||
|
value: 'selfHosted',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'https://taiga.yourdomain.com',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
environment: [
|
||||||
|
'selfHosted',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
import {
|
|
||||||
ICredentialType,
|
|
||||||
INodeProperties,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
export class TaigaCloudApi implements ICredentialType {
|
|
||||||
name = 'taigaCloudApi';
|
|
||||||
displayName = 'Taiga Cloud API';
|
|
||||||
documentationUrl = 'taiga';
|
|
||||||
properties: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Username',
|
|
||||||
name: 'username',
|
|
||||||
type: 'string',
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Password',
|
|
||||||
name: 'password',
|
|
||||||
type: 'string',
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -163,7 +163,7 @@ export function activeCampaignDefaultGetAllProperties(resource: string, operatio
|
||||||
description: 'How many results to return.',
|
description: 'How many results to return.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -177,7 +177,7 @@ export function activeCampaignDefaultGetAllProperties(resource: string, operatio
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -221,7 +221,7 @@ export class Airtable implements INodeType {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: `Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive.`,
|
description: `Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive and cannot include spaces after a comma.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Additional Options',
|
displayName: 'Additional Options',
|
||||||
|
@ -390,7 +390,7 @@ export class Airtable implements INodeType {
|
||||||
},
|
},
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// append + update
|
// append + delete + update
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
{
|
{
|
||||||
displayName: 'Options',
|
displayName: 'Options',
|
||||||
|
@ -401,12 +401,24 @@ export class Airtable implements INodeType {
|
||||||
show: {
|
show: {
|
||||||
operation: [
|
operation: [
|
||||||
'append',
|
'append',
|
||||||
|
'delete',
|
||||||
'update',
|
'update',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: {},
|
default: {},
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Bulk Size',
|
||||||
|
name: 'bulkSize',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 10,
|
||||||
|
},
|
||||||
|
default: 10,
|
||||||
|
description: `Number of records to process at once.`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Ignore Fields',
|
displayName: 'Ignore Fields',
|
||||||
name: 'ignoreFields',
|
name: 'ignoreFields',
|
||||||
|
@ -428,6 +440,14 @@ export class Airtable implements INodeType {
|
||||||
displayName: 'Typecast',
|
displayName: 'Typecast',
|
||||||
name: 'typecast',
|
name: 'typecast',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/operation': [
|
||||||
|
'append',
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
default: false,
|
default: false,
|
||||||
description: 'If the Airtable API should attempt mapping of string values for linked records & select options.',
|
description: 'If the Airtable API should attempt mapping of string values for linked records & select options.',
|
||||||
},
|
},
|
||||||
|
@ -465,41 +485,65 @@ export class Airtable implements INodeType {
|
||||||
let fields: string[];
|
let fields: string[];
|
||||||
let options: IDataObject;
|
let options: IDataObject;
|
||||||
|
|
||||||
|
const rows: IDataObject[] = [];
|
||||||
|
let bulkSize = 10;
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
addAllFields = this.getNodeParameter('addAllFields', i) as boolean;
|
addAllFields = this.getNodeParameter('addAllFields', i) as boolean;
|
||||||
options = this.getNodeParameter('options', i, {}) as IDataObject;
|
options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||||
|
bulkSize = options.bulkSize as number || bulkSize;
|
||||||
|
|
||||||
|
const row: IDataObject = {};
|
||||||
|
|
||||||
if (addAllFields === true) {
|
if (addAllFields === true) {
|
||||||
// Add all the fields the item has
|
// Add all the fields the item has
|
||||||
body.fields = items[i].json;
|
row.fields = { ...items[i].json };
|
||||||
|
// tslint:disable-next-line: no-any
|
||||||
|
delete (row.fields! as any).id;
|
||||||
} else {
|
} else {
|
||||||
// Add only the specified fields
|
// Add only the specified fields
|
||||||
body.fields = {} as IDataObject;
|
row.fields = {} as IDataObject;
|
||||||
|
|
||||||
fields = this.getNodeParameter('fields', i, []) as string[];
|
fields = this.getNodeParameter('fields', i, []) as string[];
|
||||||
|
|
||||||
for (const fieldName of fields) {
|
for (const fieldName of fields) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
body.fields[fieldName] = items[i].json[fieldName];
|
row.fields[fieldName] = items[i].json[fieldName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rows.push(row);
|
||||||
|
|
||||||
|
if (rows.length === bulkSize || i === items.length - 1) {
|
||||||
if (options.typecast === true) {
|
if (options.typecast === true) {
|
||||||
body['typecast'] = true;
|
body['typecast'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body['records'] = rows;
|
||||||
|
|
||||||
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
|
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||||
|
|
||||||
returnData.push(responseData);
|
returnData.push(...responseData.records);
|
||||||
|
// empty rows
|
||||||
|
rows.length = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (operation === 'delete') {
|
} else if (operation === 'delete') {
|
||||||
requestMethod = 'DELETE';
|
requestMethod = 'DELETE';
|
||||||
|
|
||||||
let id: string;
|
const rows: string[] = [];
|
||||||
|
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
|
||||||
|
const bulkSize = options.bulkSize as number || 10;
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
let id: string;
|
||||||
|
|
||||||
id = this.getNodeParameter('id', i) as string;
|
id = this.getNodeParameter('id', i) as string;
|
||||||
|
|
||||||
|
rows.push(id);
|
||||||
|
|
||||||
|
if (rows.length === bulkSize || i === items.length - 1) {
|
||||||
endpoint = `${application}/${table}`;
|
endpoint = `${application}/${table}`;
|
||||||
|
|
||||||
// Make one request after another. This is slower but makes
|
// Make one request after another. This is slower but makes
|
||||||
|
@ -508,11 +552,14 @@ export class Airtable implements INodeType {
|
||||||
// functionality in core should make it easy to make requests
|
// functionality in core should make it easy to make requests
|
||||||
// according to specific rules like not more than 5 requests
|
// according to specific rules like not more than 5 requests
|
||||||
// per seconds.
|
// per seconds.
|
||||||
qs.records = [id];
|
qs.records = rows;
|
||||||
|
|
||||||
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
|
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||||
|
|
||||||
returnData.push(...responseData.records);
|
returnData.push(...responseData.records);
|
||||||
|
// empty rows
|
||||||
|
rows.length = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (operation === 'list') {
|
} else if (operation === 'list') {
|
||||||
|
@ -585,41 +632,51 @@ export class Airtable implements INodeType {
|
||||||
|
|
||||||
requestMethod = 'PATCH';
|
requestMethod = 'PATCH';
|
||||||
|
|
||||||
let id: string;
|
|
||||||
let updateAllFields: boolean;
|
let updateAllFields: boolean;
|
||||||
let fields: string[];
|
let fields: string[];
|
||||||
let options: IDataObject;
|
let options: IDataObject;
|
||||||
|
|
||||||
|
const rows: IDataObject[] = [];
|
||||||
|
let bulkSize = 10;
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
updateAllFields = this.getNodeParameter('updateAllFields', i) as boolean;
|
updateAllFields = this.getNodeParameter('updateAllFields', i) as boolean;
|
||||||
options = this.getNodeParameter('options', i, {}) as IDataObject;
|
options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||||
|
bulkSize = options.bulkSize as number || bulkSize;
|
||||||
|
|
||||||
|
const row: IDataObject = {};
|
||||||
|
row.fields = {} as IDataObject;
|
||||||
|
|
||||||
if (updateAllFields === true) {
|
if (updateAllFields === true) {
|
||||||
// Update all the fields the item has
|
// Update all the fields the item has
|
||||||
body.fields = items[i].json;
|
row.fields = { ...items[i].json };
|
||||||
|
// remove id field
|
||||||
|
// tslint:disable-next-line: no-any
|
||||||
|
delete (row.fields! as any).id;
|
||||||
|
|
||||||
if (options.ignoreFields && options.ignoreFields !== '') {
|
if (options.ignoreFields && options.ignoreFields !== '') {
|
||||||
const ignoreFields = (options.ignoreFields as string).split(',').map(field => field.trim()).filter(field => !!field);
|
const ignoreFields = (options.ignoreFields as string).split(',').map(field => field.trim()).filter(field => !!field);
|
||||||
if (ignoreFields.length) {
|
if (ignoreFields.length) {
|
||||||
// From: https://stackoverflow.com/questions/17781472/how-to-get-a-subset-of-a-javascript-objects-properties
|
// From: https://stackoverflow.com/questions/17781472/how-to-get-a-subset-of-a-javascript-objects-properties
|
||||||
body.fields = Object.entries(items[i].json)
|
row.fields = Object.entries(items[i].json)
|
||||||
.filter(([key]) => !ignoreFields.includes(key))
|
.filter(([key]) => !ignoreFields.includes(key))
|
||||||
.reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {});
|
.reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update only the specified fields
|
|
||||||
body.fields = {} as IDataObject;
|
|
||||||
|
|
||||||
fields = this.getNodeParameter('fields', i, []) as string[];
|
fields = this.getNodeParameter('fields', i, []) as string[];
|
||||||
|
|
||||||
for (const fieldName of fields) {
|
for (const fieldName of fields) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
body.fields[fieldName] = items[i].json[fieldName];
|
row.fields[fieldName] = items[i].json[fieldName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id = this.getNodeParameter('id', i) as string;
|
row.id = this.getNodeParameter('id', i) as string;
|
||||||
|
|
||||||
|
rows.push(row);
|
||||||
|
|
||||||
|
if (rows.length === bulkSize || i === items.length - 1) {
|
||||||
endpoint = `${application}/${table}`;
|
endpoint = `${application}/${table}`;
|
||||||
|
|
||||||
// Make one request after another. This is slower but makes
|
// Make one request after another. This is slower but makes
|
||||||
|
@ -629,11 +686,15 @@ export class Airtable implements INodeType {
|
||||||
// according to specific rules like not more than 5 requests
|
// according to specific rules like not more than 5 requests
|
||||||
// per seconds.
|
// per seconds.
|
||||||
|
|
||||||
const data = { records: [{ id, fields: body.fields }], typecast: (options.typecast) ? true : false };
|
const data = { records: rows, typecast: (options.typecast) ? true : false };
|
||||||
|
|
||||||
responseData = await apiRequest.call(this, requestMethod, endpoint, data, qs);
|
responseData = await apiRequest.call(this, requestMethod, endpoint, data, qs);
|
||||||
|
|
||||||
returnData.push(...responseData.records);
|
returnData.push(...responseData.records);
|
||||||
|
|
||||||
|
// empty rows
|
||||||
|
rows.length = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -158,7 +158,7 @@ export class AwsComprehend implements INodeType {
|
||||||
description: 'The text to send.',
|
description: 'The text to send.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -172,7 +172,7 @@ export class AwsComprehend implements INodeType {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Additional Fields',
|
displayName: 'Additional Fields',
|
||||||
|
|
19
packages/nodes-base/nodes/Baserow/Baserow.node.json
Normal file
19
packages/nodes-base/nodes/Baserow/Baserow.node.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.baserow",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": ["Data & Storage"],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/credentials/baserow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.baserow/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generic": []
|
||||||
|
}
|
||||||
|
}
|
318
packages/nodes-base/nodes/Baserow/Baserow.node.ts
Normal file
318
packages/nodes-base/nodes/Baserow/Baserow.node.ts
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
baserowApiRequest,
|
||||||
|
baserowApiRequestAllItems,
|
||||||
|
getJwtToken,
|
||||||
|
TableFieldMapper,
|
||||||
|
toOptions,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
operationFields
|
||||||
|
} from './OperationDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BaserowCredentials,
|
||||||
|
FieldsUiValues,
|
||||||
|
GetAllAdditionalOptions,
|
||||||
|
LoadedResource,
|
||||||
|
Operation,
|
||||||
|
Row,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export class Baserow implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Baserow',
|
||||||
|
name: 'baserow',
|
||||||
|
icon: 'file:baserow.svg',
|
||||||
|
group: ['output'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Consume the Baserow API',
|
||||||
|
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
|
||||||
|
defaults: {
|
||||||
|
name: 'Baserow',
|
||||||
|
color: '#00a2ce',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'baserowApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Row',
|
||||||
|
value: 'row',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'row',
|
||||||
|
description: 'Operation to perform',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'row',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create a row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete a row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Retrieve a row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Retrieve all rows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
description: 'Update a row',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
description: 'Operation to perform',
|
||||||
|
},
|
||||||
|
...operationFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getDatabaseIds(this: ILoadOptionsFunctions) {
|
||||||
|
const credentials = this.getCredentials('baserowApi') as BaserowCredentials;
|
||||||
|
const jwtToken = await getJwtToken.call(this, credentials);
|
||||||
|
const endpoint = '/api/applications/';
|
||||||
|
const databases = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[];
|
||||||
|
return toOptions(databases);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTableIds(this: ILoadOptionsFunctions) {
|
||||||
|
const credentials = this.getCredentials('baserowApi') as BaserowCredentials;
|
||||||
|
const jwtToken = await getJwtToken.call(this, credentials);
|
||||||
|
const databaseId = this.getNodeParameter('databaseId', 0) as string;
|
||||||
|
const endpoint = `/api/database/tables/database/${databaseId}`;
|
||||||
|
const tables = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[];
|
||||||
|
return toOptions(tables);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTableFields(this: ILoadOptionsFunctions) {
|
||||||
|
const credentials = this.getCredentials('baserowApi') as BaserowCredentials;
|
||||||
|
const jwtToken = await getJwtToken.call(this, credentials);
|
||||||
|
const tableId = this.getNodeParameter('tableId', 0) as string;
|
||||||
|
const endpoint = `/api/database/fields/table/${tableId}/`;
|
||||||
|
const fields = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[];
|
||||||
|
return toOptions(fields);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const mapper = new TableFieldMapper();
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
const operation = this.getNodeParameter('operation', 0) as Operation;
|
||||||
|
|
||||||
|
const tableId = this.getNodeParameter('tableId', 0) as string;
|
||||||
|
const credentials = this.getCredentials('baserowApi') as BaserowCredentials;
|
||||||
|
const jwtToken = await getJwtToken.call(this, credentials);
|
||||||
|
const fields = await mapper.getTableFields.call(this, tableId, jwtToken);
|
||||||
|
mapper.createMappings(fields);
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// getAll
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://api.baserow.io/api/redoc/#operation/list_database_table_rows
|
||||||
|
|
||||||
|
const { order, filters, filterType, search } = this.getNodeParameter('additionalOptions', 0) as GetAllAdditionalOptions;
|
||||||
|
|
||||||
|
const qs: IDataObject = {};
|
||||||
|
|
||||||
|
if (order?.fields) {
|
||||||
|
qs['order_by'] = order.fields
|
||||||
|
.map(({ field, direction }) => `${direction}${mapper.setField(field)}`)
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.fields) {
|
||||||
|
filters.fields.forEach(({ field, operator, value }) => {
|
||||||
|
qs[`filter__field_${mapper.setField(field)}__${operator}`] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterType) {
|
||||||
|
qs.filter_type = filterType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
qs.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `/api/database/rows/table/${tableId}/`;
|
||||||
|
const rows = await baserowApiRequestAllItems.call(this, 'GET', endpoint, {}, qs, jwtToken) as Row[];
|
||||||
|
|
||||||
|
rows.forEach(row => mapper.idsToNames(row));
|
||||||
|
|
||||||
|
returnData.push(...rows);
|
||||||
|
|
||||||
|
} else if (operation === 'get') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// get
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://api.baserow.io/api/redoc/#operation/get_database_table_row
|
||||||
|
|
||||||
|
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||||
|
const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`;
|
||||||
|
const row = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken);
|
||||||
|
|
||||||
|
mapper.idsToNames(row);
|
||||||
|
|
||||||
|
returnData.push(row);
|
||||||
|
|
||||||
|
} else if (operation === 'create') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// create
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://api.baserow.io/api/redoc/#operation/create_database_table_row
|
||||||
|
|
||||||
|
const body: IDataObject = {};
|
||||||
|
|
||||||
|
const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapColumns';
|
||||||
|
|
||||||
|
if (dataToSend === 'autoMapColumns') {
|
||||||
|
|
||||||
|
const incomingKeys = Object.keys(items[i].json);
|
||||||
|
const rawInputsToIgnore = this.getNodeParameter('inputDataToIgnore', i) as string;
|
||||||
|
const inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim());
|
||||||
|
|
||||||
|
for (const key of incomingKeys) {
|
||||||
|
if (inputDataToIgnore.includes(key)) continue;
|
||||||
|
body[key] = items[i].json[key];
|
||||||
|
mapper.namesToIds(body);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues;
|
||||||
|
for (const field of fields) {
|
||||||
|
body[`field_${field.fieldId}`] = field.fieldValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `/api/database/rows/table/${tableId}/`;
|
||||||
|
const createdRow = await baserowApiRequest.call(this, 'POST', endpoint, body, {}, jwtToken);
|
||||||
|
|
||||||
|
mapper.idsToNames(createdRow);
|
||||||
|
|
||||||
|
returnData.push(createdRow);
|
||||||
|
|
||||||
|
} else if (operation === 'update') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// update
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://api.baserow.io/api/redoc/#operation/update_database_table_row
|
||||||
|
|
||||||
|
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||||
|
|
||||||
|
const body: IDataObject = {};
|
||||||
|
|
||||||
|
const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapInputData';
|
||||||
|
|
||||||
|
if (dataToSend === 'autoMapInputData') {
|
||||||
|
|
||||||
|
const incomingKeys = Object.keys(items[i].json);
|
||||||
|
const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string;
|
||||||
|
const inputsToIgnore = rawInputsToIgnore.split(',').map(c => c.trim());
|
||||||
|
|
||||||
|
for (const key of incomingKeys) {
|
||||||
|
if (inputsToIgnore.includes(key)) continue;
|
||||||
|
body[key] = items[i].json[key];
|
||||||
|
mapper.namesToIds(body);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues;
|
||||||
|
for (const field of fields) {
|
||||||
|
body[`field_${field.fieldId}`] = field.fieldValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`;
|
||||||
|
const updatedRow = await baserowApiRequest.call(this, 'PATCH', endpoint, body, {}, jwtToken);
|
||||||
|
|
||||||
|
mapper.idsToNames(updatedRow);
|
||||||
|
|
||||||
|
returnData.push(updatedRow);
|
||||||
|
|
||||||
|
} else if (operation === 'delete') {
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// delete
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// https://api.baserow.io/api/redoc/#operation/delete_database_table_row
|
||||||
|
|
||||||
|
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||||
|
|
||||||
|
const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`;
|
||||||
|
await baserowApiRequest.call(this, 'DELETE', endpoint, {}, {}, jwtToken);
|
||||||
|
|
||||||
|
returnData.push({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
|
}
|
||||||
|
}
|
198
packages/nodes-base/nodes/Baserow/GenericFunctions.ts
Normal file
198
packages/nodes-base/nodes/Baserow/GenericFunctions.ts
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
OptionsWithUri,
|
||||||
|
} from 'request';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
NodeApiError,
|
||||||
|
NodeOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Accumulator,
|
||||||
|
BaserowCredentials,
|
||||||
|
LoadedResource,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to Baserow API.
|
||||||
|
*/
|
||||||
|
export async function baserowApiRequest(
|
||||||
|
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
body: IDataObject = {},
|
||||||
|
qs: IDataObject = {},
|
||||||
|
jwtToken: string,
|
||||||
|
) {
|
||||||
|
const credentials = this.getCredentials('baserowApi') as BaserowCredentials;
|
||||||
|
|
||||||
|
if (credentials === undefined) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: OptionsWithUri = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `JWT ${jwtToken}`,
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
qs,
|
||||||
|
uri: `${credentials.host}${endpoint}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(qs).length === 0) {
|
||||||
|
delete options.qs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(body).length === 0) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.helpers.request!(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all results from a paginated query to Baserow API.
|
||||||
|
*/
|
||||||
|
export async function baserowApiRequestAllItems(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
body: IDataObject,
|
||||||
|
qs: IDataObject = {},
|
||||||
|
jwtToken: string,
|
||||||
|
): Promise<IDataObject[]> {
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
qs.page = 1;
|
||||||
|
qs.size = 100;
|
||||||
|
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
|
||||||
|
const limit = this.getNodeParameter('limit', 0, 0) as number;
|
||||||
|
|
||||||
|
do {
|
||||||
|
responseData = await baserowApiRequest.call(this, method, endpoint, body, qs, jwtToken);
|
||||||
|
returnData.push(...responseData.results);
|
||||||
|
|
||||||
|
if (!returnAll && returnData.length > limit) {
|
||||||
|
return returnData.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
qs.page += 1;
|
||||||
|
} while (responseData.next !== null);
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a JWT token based on Baserow account username and password.
|
||||||
|
*/
|
||||||
|
export async function getJwtToken(
|
||||||
|
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
{ username, password, host }: BaserowCredentials,
|
||||||
|
) {
|
||||||
|
const options: OptionsWithUri = {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
uri: `${host}/api/user/token-auth/`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { token } = await this.helpers.request!(options) as { token: string };
|
||||||
|
return token;
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFieldNamesAndIds(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
tableId: string,
|
||||||
|
jwtToken: string,
|
||||||
|
) {
|
||||||
|
const endpoint = `/api/database/fields/table/${tableId}/`;
|
||||||
|
const response = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
names: response.map((field) => field.name),
|
||||||
|
ids: response.map((field) => `field_${field.id}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toOptions = (items: LoadedResource[]) =>
|
||||||
|
items.map(({ name, id }) => ({ name, value: id }));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for mapping field IDs `field_n` to names and vice versa.
|
||||||
|
*/
|
||||||
|
export class TableFieldMapper {
|
||||||
|
nameToIdMapping: Record<string, string> = {};
|
||||||
|
idToNameMapping: Record<string, string> = {};
|
||||||
|
mapIds = true;
|
||||||
|
|
||||||
|
async getTableFields(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
table: string,
|
||||||
|
jwtToken: string,
|
||||||
|
): Promise<LoadedResource[]> {
|
||||||
|
const endpoint = `/api/database/fields/table/${table}/`;
|
||||||
|
return await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
createMappings(tableFields: LoadedResource[]) {
|
||||||
|
this.nameToIdMapping = this.createNameToIdMapping(tableFields);
|
||||||
|
this.idToNameMapping = this.createIdToNameMapping(tableFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createIdToNameMapping(responseData: LoadedResource[]) {
|
||||||
|
return responseData.reduce<Accumulator>((acc, cur) => {
|
||||||
|
acc[`field_${cur.id}`] = cur.name;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createNameToIdMapping(responseData: LoadedResource[]) {
|
||||||
|
return responseData.reduce<Accumulator>((acc, cur) => {
|
||||||
|
acc[cur.name] = `field_${cur.id}`;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
setField(field: string) {
|
||||||
|
return this.mapIds ? field : this.nameToIdMapping[field] ?? field;
|
||||||
|
}
|
||||||
|
|
||||||
|
idsToNames(obj: Record<string, unknown>) {
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
if (this.idToNameMapping[key] !== undefined) {
|
||||||
|
delete obj[key];
|
||||||
|
obj[this.idToNameMapping[key]] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
namesToIds(obj: Record<string, unknown>) {
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
if (this.nameToIdMapping[key] !== undefined) {
|
||||||
|
delete obj[key];
|
||||||
|
obj[this.nameToIdMapping[key]] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
455
packages/nodes-base/nodes/Baserow/OperationDescription.ts
Normal file
455
packages/nodes-base/nodes/Baserow/OperationDescription.ts
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const operationFields = [
|
||||||
|
// ----------------------------------
|
||||||
|
// shared
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Database',
|
||||||
|
name: 'databaseId',
|
||||||
|
type: 'options',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Database to operate on',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDatabaseIds',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Table',
|
||||||
|
name: 'tableId',
|
||||||
|
type: 'options',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Table to operate on',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'databaseId',
|
||||||
|
],
|
||||||
|
loadOptionsMethod: 'getTableIds',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// get
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Row ID',
|
||||||
|
name: 'rowId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the row to return',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// update
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Row ID',
|
||||||
|
name: 'rowId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the row to update',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// create/update
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Data to Send',
|
||||||
|
name: 'dataToSend',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Auto-map Input Data to Columns',
|
||||||
|
value: 'autoMapInputData',
|
||||||
|
description: 'Use when node input properties match destination column names',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Define Below for Each Column',
|
||||||
|
value: 'defineBelow',
|
||||||
|
description: 'Set the value for each destination column',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 'defineBelow',
|
||||||
|
description: 'Whether to insert the input data this node receives in the new row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Inputs to Ignore',
|
||||||
|
name: 'inputsToIgnore',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
dataToSend: [
|
||||||
|
'autoMapInputData',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: false,
|
||||||
|
description: 'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
|
||||||
|
placeholder: 'Enter properties...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Fields to Send',
|
||||||
|
name: 'fieldsUi',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValueButtonText: 'Add Field to Send',
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'create',
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
dataToSend: [
|
||||||
|
'defineBelow',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Field',
|
||||||
|
name: 'fieldValues',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Field ID',
|
||||||
|
name: 'fieldId',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'tableId',
|
||||||
|
],
|
||||||
|
loadOptionsMethod: 'getTableFields',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Field Value',
|
||||||
|
name: 'fieldValue',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// delete
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Row ID',
|
||||||
|
name: 'rowId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'delete',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'ID of the row to delete',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// getAll
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 50,
|
||||||
|
description: 'How many results to return',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 100,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'additionalOptions',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Filters',
|
||||||
|
name: 'filters',
|
||||||
|
placeholder: 'Add Filter',
|
||||||
|
description: 'Filter rows based on comparison operators',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'fields',
|
||||||
|
displayName: 'Field',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Field',
|
||||||
|
name: 'field',
|
||||||
|
type: 'options',
|
||||||
|
default: '',
|
||||||
|
description: 'Field to compare',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'tableId',
|
||||||
|
],
|
||||||
|
loadOptionsMethod: 'getTableFields',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Filter',
|
||||||
|
name: 'operator',
|
||||||
|
description: 'Operator to compare field and value with',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
description: 'Field is equal to value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'not_equal',
|
||||||
|
description: 'Field is not equal to value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date Equal',
|
||||||
|
value: 'date_equal',
|
||||||
|
description: 'Field is date. Format: \'YYYY-MM-DD\'',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date Not Equal',
|
||||||
|
value: 'date_not_equal',
|
||||||
|
description: 'Field is not date. Format: \'YYYY-MM-DD\'',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date Equals Today',
|
||||||
|
value: 'date_equals_today',
|
||||||
|
description: 'Field is today. Format: string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date Equals Month',
|
||||||
|
value: 'date_equals_month',
|
||||||
|
description: 'Field in this month. Format: string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date Equals Year',
|
||||||
|
value: 'date_equals_year',
|
||||||
|
description: 'Field in this year. Format: string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Contains',
|
||||||
|
value: 'contains',
|
||||||
|
description: 'Field contains value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'File Name Contains',
|
||||||
|
value: 'filename_contains',
|
||||||
|
description: 'Field filename contains value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Contains Not',
|
||||||
|
value: 'contains_not',
|
||||||
|
description: 'Field does not contain value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Higher Than',
|
||||||
|
value: 'higher_than',
|
||||||
|
description: 'Field is higher than value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lower Than',
|
||||||
|
value: 'lower_than',
|
||||||
|
description: 'Field is lower than value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Single Select Equal',
|
||||||
|
value: 'single_select_equal',
|
||||||
|
description: 'Field selected option is value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Single Select Not Equal',
|
||||||
|
value: 'single_select_not_equal',
|
||||||
|
description: 'Field selected option is not value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Is True',
|
||||||
|
value: 'boolean',
|
||||||
|
description: 'Boolean field is true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Is Empty',
|
||||||
|
value: 'empty',
|
||||||
|
description: 'Field is empty',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Empty',
|
||||||
|
value: 'not_empty',
|
||||||
|
description: 'Field is not empty',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Value to compare to',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Filter Type',
|
||||||
|
name: 'filterType',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'AND',
|
||||||
|
value: 'AND',
|
||||||
|
description: 'Indicates that the rows must match all the provided filters',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OR',
|
||||||
|
value: 'OR',
|
||||||
|
description: 'Indicates that the rows only have to match one of the filters',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'AND',
|
||||||
|
description: 'This works only if two or more filters are provided. Defaults to <code>AND</code>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Search Term',
|
||||||
|
name: 'search',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Text to match (can be in any column)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Sorting',
|
||||||
|
name: 'order',
|
||||||
|
placeholder: 'Add Sort Order',
|
||||||
|
description: 'Set the sort order of the result rows',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Fields',
|
||||||
|
displayName: 'Field',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Field Name',
|
||||||
|
name: 'field',
|
||||||
|
type: 'options',
|
||||||
|
default: '',
|
||||||
|
description: 'Field name to sort by',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'tableId',
|
||||||
|
],
|
||||||
|
loadOptionsMethod: 'getTableFields',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Direction',
|
||||||
|
name: 'direction',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'ASC',
|
||||||
|
value: '',
|
||||||
|
description: 'Sort in ascending order',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DESC',
|
||||||
|
value: '-',
|
||||||
|
description: 'Sort in descending order',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: '',
|
||||||
|
description: 'Sort direction, either ascending or descending',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
143
packages/nodes-base/nodes/Baserow/baserow.svg
Normal file
143
packages/nodes-base/nodes/Baserow/baserow.svg
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg:svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
viewBox="0 0 60 60.000001"
|
||||||
|
version="1.1"
|
||||||
|
id="svg14"
|
||||||
|
sodipodi:docname="baserow.svg"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||||
|
<svg:metadata
|
||||||
|
id="metadata20">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</svg:metadata>
|
||||||
|
<svg:defs
|
||||||
|
id="defs18" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1848"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
id="namedview16"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="2.5579745"
|
||||||
|
inkscape:cx="-29.906474"
|
||||||
|
inkscape:cy="40.001009"
|
||||||
|
inkscape:window-x="1512"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Group_163" />
|
||||||
|
<svg:g
|
||||||
|
id="Group_163"
|
||||||
|
data-name="Group 163"
|
||||||
|
transform="translate(-451.976,-540.719)">
|
||||||
|
<svg:g
|
||||||
|
id="Group_162"
|
||||||
|
data-name="Group 162"
|
||||||
|
transform="matrix(1.8935807,0,0,1.8935807,451.976,543.89927)"
|
||||||
|
style="stroke-width:0.52810001">
|
||||||
|
<svg:g
|
||||||
|
id="Group_161"
|
||||||
|
data-name="Group 161"
|
||||||
|
style="stroke-width:0.52810001">
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_257"
|
||||||
|
data-name="Rectangle 257"
|
||||||
|
width="21.863001"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(6.654,24.335)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#566270;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_258"
|
||||||
|
data-name="Rectangle 258"
|
||||||
|
width="23.955"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(0,18.251)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#4570a9;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_259"
|
||||||
|
data-name="Rectangle 259"
|
||||||
|
width="6.4640002"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(25.222,18.251)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#4570a9;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_260"
|
||||||
|
data-name="Rectangle 260"
|
||||||
|
width="11.597"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(4.563,12.167)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#00a2ce;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_261"
|
||||||
|
data-name="Rectangle 261"
|
||||||
|
width="5.2280002"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(17.728,12.167)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#00a2ce;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_262"
|
||||||
|
data-name="Rectangle 262"
|
||||||
|
width="19.392"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(8.365,6.084)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#6acaff;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_263"
|
||||||
|
data-name="Rectangle 263"
|
||||||
|
width="16.35"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(7.319)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#9addff;stroke-width:0.52810001" />
|
||||||
|
<svg:rect
|
||||||
|
id="Rectangle_264"
|
||||||
|
data-name="Rectangle 264"
|
||||||
|
width="5.2280002"
|
||||||
|
height="3.9920001"
|
||||||
|
transform="translate(0.285)"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill:#9addff;stroke-width:0.52810001" />
|
||||||
|
</svg:g>
|
||||||
|
</svg:g>
|
||||||
|
</svg:g>
|
||||||
|
<script />
|
||||||
|
</svg:svg>
|
After Width: | Height: | Size: 4.2 KiB |
41
packages/nodes-base/nodes/Baserow/types.d.ts
vendored
Normal file
41
packages/nodes-base/nodes/Baserow/types.d.ts
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
export type BaserowCredentials = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
host: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAllAdditionalOptions = {
|
||||||
|
order?: {
|
||||||
|
fields: Array<{
|
||||||
|
field: string;
|
||||||
|
direction: string;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
filters?: {
|
||||||
|
fields: Array<{
|
||||||
|
field: string;
|
||||||
|
operator: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
filterType: string,
|
||||||
|
search: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoadedResource = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Accumulator = {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Row = Record<string, string>
|
||||||
|
|
||||||
|
export type FieldsUiValues = Array<{
|
||||||
|
fieldId: string;
|
||||||
|
fieldValue: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Operation = 'create' | 'delete' | 'update' | 'get' | 'getAll';
|
|
@ -29,6 +29,10 @@ import {
|
||||||
|
|
||||||
import * as moment from 'moment-timezone';
|
import * as moment from 'moment-timezone';
|
||||||
|
|
||||||
|
import {
|
||||||
|
noCase,
|
||||||
|
} from 'change-case';
|
||||||
|
|
||||||
export class Box implements INodeType {
|
export class Box implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Box',
|
displayName: 'Box',
|
||||||
|
@ -81,6 +85,7 @@ export class Box implements INodeType {
|
||||||
const length = items.length as unknown as number;
|
const length = items.length as unknown as number;
|
||||||
const qs: IDataObject = {};
|
const qs: IDataObject = {};
|
||||||
let responseData;
|
let responseData;
|
||||||
|
const timezone = this.getTimezone();
|
||||||
const resource = this.getNodeParameter('resource', 0) as string;
|
const resource = this.getNodeParameter('resource', 0) as string;
|
||||||
const operation = this.getNodeParameter('operation', 0) as string;
|
const operation = this.getNodeParameter('operation', 0) as string;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
|
@ -199,6 +204,51 @@ export class Box implements INodeType {
|
||||||
}
|
}
|
||||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||||
}
|
}
|
||||||
|
// https://developer.box.com/reference/post-collaborations/
|
||||||
|
if (operation === 'share') {
|
||||||
|
const fileId = this.getNodeParameter('fileId', i) as string;
|
||||||
|
const role = this.getNodeParameter('role', i) as string;
|
||||||
|
const accessibleBy = this.getNodeParameter('accessibleBy', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
// tslint:disable-next-line: no-any
|
||||||
|
const body: { accessible_by: IDataObject, [key: string]: any } = {
|
||||||
|
accessible_by: {},
|
||||||
|
item: {
|
||||||
|
id: fileId,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
role: (role === 'coOwner') ? 'co-owner' : noCase(role),
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.fields) {
|
||||||
|
qs.fields = body.fields;
|
||||||
|
delete body.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.expires_at) {
|
||||||
|
body.expires_at = moment.tz(body.expires_at, timezone).format();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.notify) {
|
||||||
|
qs.notify = body.notify;
|
||||||
|
delete body.notify;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessibleBy === 'user') {
|
||||||
|
const useEmail = this.getNodeParameter('useEmail', i) as boolean;
|
||||||
|
if (useEmail) {
|
||||||
|
body.accessible_by['login'] = this.getNodeParameter('email', i) as string;
|
||||||
|
} else {
|
||||||
|
body.accessible_by['id'] = this.getNodeParameter('userId', i) as string;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.accessible_by['id'] = this.getNodeParameter('groupId', i) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await boxApiRequest.call(this, 'POST', `/collaborations`, body, qs);
|
||||||
|
returnData.push(responseData as IDataObject);
|
||||||
|
}
|
||||||
// https://developer.box.com/reference/post-files-content
|
// https://developer.box.com/reference/post-files-content
|
||||||
if (operation === 'upload') {
|
if (operation === 'upload') {
|
||||||
const parentId = this.getNodeParameter('parentId', i) as string;
|
const parentId = this.getNodeParameter('parentId', i) as string;
|
||||||
|
@ -356,6 +406,79 @@ export class Box implements INodeType {
|
||||||
}
|
}
|
||||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||||
}
|
}
|
||||||
|
// https://developer.box.com/reference/post-collaborations/
|
||||||
|
if (operation === 'share') {
|
||||||
|
const folderId = this.getNodeParameter('folderId', i) as string;
|
||||||
|
const role = this.getNodeParameter('role', i) as string;
|
||||||
|
const accessibleBy = this.getNodeParameter('accessibleBy', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
// tslint:disable-next-line: no-any
|
||||||
|
const body: { accessible_by: IDataObject, [key: string]: any } = {
|
||||||
|
accessible_by: {},
|
||||||
|
item: {
|
||||||
|
id: folderId,
|
||||||
|
type: 'folder',
|
||||||
|
},
|
||||||
|
role: (role === 'coOwner') ? 'co-owner' : noCase(role),
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.fields) {
|
||||||
|
qs.fields = body.fields;
|
||||||
|
delete body.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.expires_at) {
|
||||||
|
body.expires_at = moment.tz(body.expires_at, timezone).format();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.notify) {
|
||||||
|
qs.notify = body.notify;
|
||||||
|
delete body.notify;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessibleBy === 'user') {
|
||||||
|
const useEmail = this.getNodeParameter('useEmail', i) as boolean;
|
||||||
|
if (useEmail) {
|
||||||
|
body.accessible_by['login'] = this.getNodeParameter('email', i) as string;
|
||||||
|
} else {
|
||||||
|
body.accessible_by['id'] = this.getNodeParameter('userId', i) as string;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.accessible_by['id'] = this.getNodeParameter('groupId', i) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await boxApiRequest.call(this, 'POST', `/collaborations`, body, qs);
|
||||||
|
returnData.push(responseData as IDataObject);
|
||||||
|
}
|
||||||
|
//https://developer.box.com/guides/folders/single/move/
|
||||||
|
if (operation === 'update') {
|
||||||
|
const folderId = this.getNodeParameter('folderId', i) as string;
|
||||||
|
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
|
||||||
|
|
||||||
|
if (updateFields.fields) {
|
||||||
|
qs.fields = updateFields.fields;
|
||||||
|
delete updateFields.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
...updateFields,
|
||||||
|
} as IDataObject;
|
||||||
|
|
||||||
|
if (body.parentId) {
|
||||||
|
body.parent = {
|
||||||
|
id: body.parentId,
|
||||||
|
};
|
||||||
|
delete body.parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.tags) {
|
||||||
|
body.tags = (body.tags as string).split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await boxApiRequest.call(this, 'PUT', `/folders/${folderId}`, body, qs);
|
||||||
|
returnData.push(responseData as IDataObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (resource === 'file' && operation === 'download') {
|
if (resource === 'file' && operation === 'download') {
|
||||||
|
|
|
@ -21,7 +21,7 @@ export class BoxTrigger implements INodeType {
|
||||||
icon: 'file:box.png',
|
icon: 'file:box.png',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Starts the workflow when a Box events occurs.',
|
description: 'Starts the workflow when Box events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Box Trigger',
|
name: 'Box Trigger',
|
||||||
color: '#00aeef',
|
color: '#00aeef',
|
||||||
|
|
|
@ -40,6 +40,11 @@ export const fileOperations = [
|
||||||
value: 'search',
|
value: 'search',
|
||||||
description: 'Search files',
|
description: 'Search files',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Share',
|
||||||
|
value: 'share',
|
||||||
|
description: 'Share a file',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Upload',
|
name: 'Upload',
|
||||||
value: 'upload',
|
value: 'upload',
|
||||||
|
@ -496,6 +501,242 @@ export const fileFields = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* file:share */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'File ID',
|
||||||
|
name: 'fileId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The ID of the file to share.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Accessible By',
|
||||||
|
name: 'accessibleBy',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Group',
|
||||||
|
value: 'group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User',
|
||||||
|
value: 'user',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The type of object the file will be shared with.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Use Email',
|
||||||
|
name: 'useEmail',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
description: 'Whether identify the user by email or ID.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Email',
|
||||||
|
name: 'email',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
useEmail: [
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The user's email address to share the file with.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User ID',
|
||||||
|
name: 'userId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
useEmail: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The user's ID to share the file with.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Group ID',
|
||||||
|
name: 'groupId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'group',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The group's ID to share the file with.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Role',
|
||||||
|
name: 'role',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Co-Owner',
|
||||||
|
value: 'coOwner',
|
||||||
|
description: 'A Co-owner has all of functional read/write access that an editor does',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Editor',
|
||||||
|
value: 'editor',
|
||||||
|
description: 'An editor has full read/write access to a folder or file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Previewer',
|
||||||
|
value: 'previewer',
|
||||||
|
description: 'A previewer has limited read access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Previewer Uploader',
|
||||||
|
value: 'previewerUploader',
|
||||||
|
description: 'This access level is a combination of Previewer and Uploader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Uploader',
|
||||||
|
value: 'uploader',
|
||||||
|
description: 'An uploader has limited write access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Viewer',
|
||||||
|
value: 'viewer',
|
||||||
|
description: 'A viewer has read access to a folder or file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Viewer Uploader',
|
||||||
|
value: 'viewerUploader',
|
||||||
|
description: 'This access level is a combination of Viewer and Uploader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 'editor',
|
||||||
|
description: 'The level of access granted.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Can View Path',
|
||||||
|
name: 'can_view_path',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: `Whether the invited users can see the entire parent path to the associated folder.</br>
|
||||||
|
The user will not gain privileges in any parent folder and therefore cannot see content the user is not collaborated on.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Expires At',
|
||||||
|
name: 'expires_at',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'Set the expiration date for the collaboration. At this date, the collaboration will be automatically removed from the item.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Fields',
|
||||||
|
name: 'fields',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Notify',
|
||||||
|
name: 'notify',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether if users should receive email notification for the action performed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* file:upload */
|
/* file:upload */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
|
@ -35,6 +35,16 @@ export const folderOperations = [
|
||||||
value: 'search',
|
value: 'search',
|
||||||
description: 'Search files',
|
description: 'Search files',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Share',
|
||||||
|
value: 'share',
|
||||||
|
description: 'Share a folder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
description: 'Update folder',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
default: 'create',
|
default: 'create',
|
||||||
description: 'The operation to perform.',
|
description: 'The operation to perform.',
|
||||||
|
@ -147,6 +157,7 @@ export const folderFields = [
|
||||||
default: '',
|
default: '',
|
||||||
description: 'Folder ID',
|
description: 'Folder ID',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* folder:delete */
|
/* folder:delete */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
@ -441,4 +452,417 @@ export const folderFields = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* folder:share */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Folder ID',
|
||||||
|
name: 'folderId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The ID of the folder to share.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Accessible By',
|
||||||
|
name: 'accessibleBy',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'User',
|
||||||
|
value: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Group',
|
||||||
|
value: 'group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 'user',
|
||||||
|
description: 'The type of object the file will be shared with.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Use Email',
|
||||||
|
name: 'useEmail',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
description: 'Whether identify the user by email or ID.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Email',
|
||||||
|
name: 'email',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
useEmail: [
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The user's email address to share the folder with.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User ID',
|
||||||
|
name: 'userId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'user',
|
||||||
|
],
|
||||||
|
useEmail: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The user's ID to share the folder with.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Group ID',
|
||||||
|
name: 'groupId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
accessibleBy: [
|
||||||
|
'group',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The group's ID to share the folder with.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Role',
|
||||||
|
name: 'role',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Co-Owner',
|
||||||
|
value: 'coOwner',
|
||||||
|
description: 'A Co-owner has all of functional read/write access that an editor does',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Editor',
|
||||||
|
value: 'editor',
|
||||||
|
description: 'An editor has full read/write access to a folder or file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Previewer',
|
||||||
|
value: 'previewer',
|
||||||
|
description: 'A previewer has limited read access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Previewer Uploader',
|
||||||
|
value: 'previewerUploader',
|
||||||
|
description: 'This access level is a combination of Previewer and Uploader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Uploader',
|
||||||
|
value: 'uploader',
|
||||||
|
description: 'An uploader has limited write access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Viewer',
|
||||||
|
value: 'viewer',
|
||||||
|
description: 'A viewer has read access to a folder or file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Viewer Uploader',
|
||||||
|
value: 'viewerUploader',
|
||||||
|
description: 'This access level is a combination of Viewer and Uploader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 'editor',
|
||||||
|
description: 'The level of access granted.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Can View Path',
|
||||||
|
name: 'can_view_path',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: `Whether the invited users can see the entire parent path to the associated folder.</br>
|
||||||
|
The user will not gain privileges in any parent folder and therefore cannot see content the user is not collaborated on.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Expires At',
|
||||||
|
name: 'expires_at',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'Set the expiration date for the collaboration. At this date, the collaboration will be automatically removed from the item.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Fields',
|
||||||
|
name: 'fields',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Notify',
|
||||||
|
name: 'notify',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether if users should receive email notification for the action performed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* folder:update */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Folder ID',
|
||||||
|
name: 'folderId',
|
||||||
|
required: true,
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Folder ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Update Fields',
|
||||||
|
name: 'updateFields',
|
||||||
|
type: 'collection',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Can Non-Owners Invite',
|
||||||
|
name: 'can_non_owners_invite',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Specifies if users who are not the owner of the folder can invite new collaborators to the folder.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Can Non-Owners View Colaborators',
|
||||||
|
name: 'can_non_owners_view_collaborators',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Restricts collaborators who are not the owner of this folder from viewing other collaborations on this folder.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Description',
|
||||||
|
name: 'description',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The optional description of this folder.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Fields',
|
||||||
|
name: 'fields',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Is Collaboration Restricted To Enterprise',
|
||||||
|
name: 'is_collaboration_restricted_to_enterprise',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Specifies if new invites to this folder are restricted to users within the enterprise. This does not affect existing collaborations.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The optional new name for this folder.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Parent ID',
|
||||||
|
name: 'parentId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The parent folder for this folder. Use this to move the folder or to restore it out of the trash.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Shared Link',
|
||||||
|
name: 'shared_link',
|
||||||
|
type: 'collection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: false,
|
||||||
|
},
|
||||||
|
description: 'Share link information.',
|
||||||
|
placeholder: 'Add Shared Link Config',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Access',
|
||||||
|
name: 'access',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Collaborators',
|
||||||
|
value: 'collaborators',
|
||||||
|
description: 'Only those who have been invited to the folder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Company',
|
||||||
|
value: 'company',
|
||||||
|
description: 'only people within the company',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Open',
|
||||||
|
value: 'open',
|
||||||
|
description: 'Anyone with the link',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Password',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
access: [
|
||||||
|
'open',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The password required to access the shared link. Set the password to null to remove it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Permissions',
|
||||||
|
name: 'permissions',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Permition',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Can Download',
|
||||||
|
name: 'can_download',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'If the shared link allows for downloading of files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Unshared At',
|
||||||
|
name: 'unshared_at',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'The timestamp at which this shared link will expire.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Vanity Name',
|
||||||
|
name: 'vanity_name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Defines a custom vanity name to use in the shared link URL, for example https://app.box.com/v/my-shared-link.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tags',
|
||||||
|
name: 'tags',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The tags for this item. These tags are shown in the Box web app and mobile apps next to an item.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
] as INodeProperties[];
|
] as INodeProperties[];
|
||||||
|
|
|
@ -20,7 +20,7 @@ export class CalendlyTrigger implements INodeType {
|
||||||
icon: 'file:calendly.svg',
|
icon: 'file:calendly.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Starts the workflow when Calendly events occur.',
|
description: 'Starts the workflow when Calendly events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Calendly Trigger',
|
name: 'Calendly Trigger',
|
||||||
color: '#374252',
|
color: '#374252',
|
||||||
|
|
|
@ -17,7 +17,7 @@ export class ChargebeeTrigger implements INodeType {
|
||||||
icon: 'file:chargebee.png',
|
icon: 'file:chargebee.png',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Starts the workflow when Chargebee events occur.',
|
description: 'Starts the workflow when Chargebee events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Chargebee Trigger',
|
name: 'Chargebee Trigger',
|
||||||
color: '#559922',
|
color: '#559922',
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class ClockifyTrigger implements INodeType {
|
||||||
name: 'clockifyTrigger',
|
name: 'clockifyTrigger',
|
||||||
group: [ 'trigger' ],
|
group: [ 'trigger' ],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Watches Clockify For Events',
|
description: 'Listens to Clockify events',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Clockify Trigger',
|
name: 'Clockify Trigger',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
|
|
|
@ -48,7 +48,7 @@ export class ConvertKit implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume ConvertKit API.',
|
description: 'Consume ConvertKit API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'ConvertKit',
|
name: 'ConvertKit',
|
||||||
color: '#fb6970',
|
color: '#fb6970',
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class CrateDb implements INodeType {
|
||||||
icon: 'file:cratedb.png',
|
icon: 'file:cratedb.png',
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Add and update data in CrateDB.',
|
description: 'Add and update data in CrateDB',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'CrateDB',
|
name: 'CrateDB',
|
||||||
color: '#47889f',
|
color: '#47889f',
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class CustomerIoTrigger implements INodeType {
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
icon: 'file:customerio.svg',
|
icon: 'file:customerio.svg',
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Starts the workflow on a Customer.io update. (Beta)',
|
description: 'Starts the workflow on a Customer.io update (Beta)',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Customer.io Trigger',
|
name: 'Customer.io Trigger',
|
||||||
color: '#ffcd00',
|
color: '#ffcd00',
|
||||||
|
|
|
@ -55,7 +55,7 @@ export class Discourse implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Discourse API.',
|
description: 'Consume Discourse API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Discourse',
|
name: 'Discourse',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const searchFields: INodeProperties[] = [
|
||||||
description: 'Term to search for.',
|
description: 'Term to search for.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -64,6 +64,6 @@ export const searchFields: INodeProperties[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -515,7 +515,7 @@ export class Egoi implements INodeType {
|
||||||
description: 'How many results to return.',
|
description: 'How many results to return.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -530,7 +530,7 @@ export class Egoi implements INodeType {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simple version of the response will be returned else the RAW data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,7 +35,7 @@ export class EmailReadImap implements INodeType {
|
||||||
icon: 'fa:inbox',
|
icon: 'fa:inbox',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Triggers the workflow when a new email gets received',
|
description: 'Triggers the workflow when a new email is received',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'IMAP Email',
|
name: 'IMAP Email',
|
||||||
color: '#44AA22',
|
color: '#44AA22',
|
||||||
|
|
|
@ -51,7 +51,7 @@ export class ExecuteCommand implements INodeType {
|
||||||
icon: 'fa:terminal',
|
icon: 'fa:terminal',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Executes a command on the host.',
|
description: 'Executes a command on the host',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Execute Command',
|
name: 'Execute Command',
|
||||||
color: '#886644',
|
color: '#886644',
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class FacebookTrigger implements INodeType {
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}',
|
subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}',
|
||||||
description: 'Starts the workflow when a Facebook events occurs.',
|
description: 'Starts the workflow when Facebook events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Facebook Trigger',
|
name: 'Facebook Trigger',
|
||||||
color: '#3B5998',
|
color: '#3B5998',
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class FileMaker implements INodeType {
|
||||||
icon: 'file:filemaker.png',
|
icon: 'file:filemaker.png',
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Retrieve data from FileMaker data API.',
|
description: 'Retrieve data from the FileMaker data API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'FileMaker',
|
name: 'FileMaker',
|
||||||
color: '#665533',
|
color: '#665533',
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class Ftp implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}',
|
subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}',
|
||||||
description: 'Transfers files via FTP or SFTP.',
|
description: 'Transfers files via FTP or SFTP',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'FTP',
|
name: 'FTP',
|
||||||
color: '#303050',
|
color: '#303050',
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class Function implements INodeType {
|
||||||
icon: 'fa:code',
|
icon: 'fa:code',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Run custom function code which gets executed once and allows to add, remove, change and replace items.',
|
description: 'Run custom function code which gets executed once and allows you to add, remove, change and replace items',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Function',
|
name: 'Function',
|
||||||
color: '#FF9922',
|
color: '#FF9922',
|
||||||
|
|
|
@ -17,7 +17,7 @@ export class FunctionItem implements INodeType {
|
||||||
icon: 'fa:code',
|
icon: 'fa:code',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Run custom function code which gets executed once per item.',
|
description: 'Run custom function code which gets executed once per item',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'FunctionItem',
|
name: 'FunctionItem',
|
||||||
color: '#ddbb33',
|
color: '#ddbb33',
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class GetResponse implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume GetResponse API.',
|
description: 'Consume GetResponse API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'GetResponse',
|
name: 'GetResponse',
|
||||||
color: '#00afec',
|
color: '#00afec',
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class GetResponseTrigger implements INodeType {
|
||||||
icon: 'file:getResponse.png',
|
icon: 'file:getResponse.png',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Starts the workflow when GetResponse events occur.',
|
description: 'Starts the workflow when GetResponse events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'GetResponse Trigger',
|
name: 'GetResponse Trigger',
|
||||||
color: '#00afec',
|
color: '#00afec',
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class Ghost implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Ghost API.',
|
description: 'Consume Ghost API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Ghost',
|
name: 'Ghost',
|
||||||
color: '#15212a',
|
color: '#15212a',
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class Github implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume GitHub API.',
|
description: 'Consume GitHub API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class GithubTrigger implements INodeType {
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
|
subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
|
||||||
description: 'Starts the workflow when a Github events occurs.',
|
description: 'Starts the workflow when Github events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Github Trigger',
|
name: 'Github Trigger',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class Gitlab implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Retrieve data from GitLab API.',
|
description: 'Retrieve data from GitLab API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Gitlab',
|
name: 'Gitlab',
|
||||||
color: '#FC6D27',
|
color: '#FC6D27',
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class GitlabTrigger implements INodeType {
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
|
subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
|
||||||
description: 'Starts the workflow when a GitLab event occurs.',
|
description: 'Starts the workflow when GitLab events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Gitlab Trigger',
|
name: 'Gitlab Trigger',
|
||||||
color: '#FC6D27',
|
color: '#FC6D27',
|
||||||
|
|
|
@ -91,7 +91,7 @@ export const reportFields = [
|
||||||
description: 'How many results to return.',
|
description: 'How many results to return.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -105,7 +105,7 @@ export const reportFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Additional Fields',
|
displayName: 'Additional Fields',
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class GoogleBigQuery implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Google BigQuery API.',
|
description: 'Consume Google BigQuery API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Google BigQuery',
|
name: 'Google BigQuery',
|
||||||
color: '#3E87E4',
|
color: '#3E87E4',
|
||||||
|
|
|
@ -287,7 +287,7 @@ export const recordFields = [
|
||||||
description: 'How many results to return.',
|
description: 'How many results to return.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
|
|
@ -30,6 +30,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
|
||||||
if (Object.keys(body).length === 0) {
|
if (Object.keys(body).length === 0) {
|
||||||
delete options.body;
|
delete options.body;
|
||||||
}
|
}
|
||||||
|
console.log(options);
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
return await this.helpers.requestOAuth2.call(this, 'googleCalendarOAuth2Api', options);
|
return await this.helpers.requestOAuth2.call(this, 'googleCalendarOAuth2Api', options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class GoogleCalendar implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Google Calendar API.',
|
description: 'Consume Google Calendar API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Google Calendar',
|
name: 'Google Calendar',
|
||||||
color: '#3E87E4',
|
color: '#3E87E4',
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class GoogleContacts implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Google Contacts API.',
|
description: 'Consume Google Contacts API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Google Contacts',
|
name: 'Google Contacts',
|
||||||
color: '#1a73e8',
|
color: '#1a73e8',
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
// group: ['trigger'],
|
// group: ['trigger'],
|
||||||
// version: 1,
|
// version: 1,
|
||||||
// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
|
// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
|
||||||
// description: 'Starts the workflow when a file on Google Drive got changed.',
|
// description: 'Starts the workflow when a file on Google Drive is changed',
|
||||||
// defaults: {
|
// defaults: {
|
||||||
// name: 'Google Drive Trigger',
|
// name: 'Google Drive Trigger',
|
||||||
// color: '#3f87f2',
|
// color: '#3f87f2',
|
||||||
|
|
|
@ -137,7 +137,7 @@ export const documentFields = [
|
||||||
placeholder: 'productId, modelName, description',
|
placeholder: 'productId, modelName, description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -151,7 +151,7 @@ export const documentFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
@ -233,7 +233,7 @@ export const documentFields = [
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -247,7 +247,7 @@ export const documentFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
@ -353,7 +353,7 @@ export const documentFields = [
|
||||||
description: 'How many results to return.',
|
description: 'How many results to return.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -367,7 +367,7 @@ export const documentFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
@ -726,7 +726,7 @@ export const documentFields = [
|
||||||
placeholder: '{"structuredQuery": {"where": {"fieldFilter": {"field": {"fieldPath": "age"},"op": "EQUAL", "value": {"integerValue": 28}}}, "from": [{"collectionId": "users-collection"}]}}',
|
placeholder: '{"structuredQuery": {"where": {"fieldFilter": {"field": {"fieldPath": "age"},"op": "EQUAL", "value": {"integerValue": 28}}}, "from": [{"collectionId": "users-collection"}]}}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Simple',
|
displayName: 'Simplify Response',
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -740,6 +740,6 @@ export const documentFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: true,
|
default: true,
|
||||||
description: 'When set to true a simplify version of the response will be used else the raw data.',
|
description: 'Return a simplified version of the response instead of the raw data.',
|
||||||
},
|
},
|
||||||
] as INodeProperties[];
|
] as INodeProperties[];
|
||||||
|
|
|
@ -486,8 +486,9 @@ export class GoogleSheet {
|
||||||
inputData.forEach((item) => {
|
inputData.forEach((item) => {
|
||||||
rowData = [];
|
rowData = [];
|
||||||
keyColumnOrder.forEach((key) => {
|
keyColumnOrder.forEach((key) => {
|
||||||
if (item.hasOwnProperty(key) && item[key]) {
|
const data = item[key];
|
||||||
rowData.push(item[key]!.toString());
|
if (item.hasOwnProperty(key) && data !== null && typeof data !== 'undefined') {
|
||||||
|
rowData.push(data.toString());
|
||||||
} else {
|
} else {
|
||||||
rowData.push('');
|
rowData.push('');
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class GoogleTasks implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Google Tasks API.',
|
description: 'Consume Google Tasks API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Google Tasks',
|
name: 'Google Tasks',
|
||||||
color: '#3E87E4',
|
color: '#3E87E4',
|
||||||
|
|
|
@ -55,7 +55,7 @@ export class YouTube implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume YouTube API.',
|
description: 'Consume YouTube API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'YouTube',
|
name: 'YouTube',
|
||||||
color: '#FF0000',
|
color: '#FF0000',
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class Gotify implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Gotify API.',
|
description: 'Consume Gotify API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Gotify',
|
name: 'Gotify',
|
||||||
color: '#71c8ec',
|
color: '#71c8ec',
|
||||||
|
|
|
@ -64,7 +64,7 @@ export class HelpScout implements INodeType {
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Help Scout API.',
|
description: 'Consume HelpScout API',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'HelpScout',
|
name: 'HelpScout',
|
||||||
color: '#1392ee',
|
color: '#1392ee',
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class HelpScoutTrigger implements INodeType {
|
||||||
icon: 'file:helpScout.svg',
|
icon: 'file:helpScout.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Starts the workflow when HelpScout events occur.',
|
description: 'Starts the workflow when HelpScout events occur',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'HelpScout Trigger',
|
name: 'HelpScout Trigger',
|
||||||
color: '#1392ee',
|
color: '#1392ee',
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import {
|
||||||
|
INodeProperties
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const cameraProxyOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'cameraProxy',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get Screenshot',
|
||||||
|
value: 'getScreenshot',
|
||||||
|
description: 'Get the camera screenshot',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getScreenshot',
|
||||||
|
description: 'The operation to perform.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const cameraProxyFields = [
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* cameraProxy:getScreenshot */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Camera Entity ID',
|
||||||
|
name: 'cameraEntityId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getScreenshot',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'cameraProxy',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'The camera entity ID.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Binary Property',
|
||||||
|
name: 'binaryPropertyName',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: 'data',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getScreenshot',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'cameraProxy',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
32
packages/nodes-base/nodes/HomeAssistant/ConfigDescription.ts
Normal file
32
packages/nodes-base/nodes/HomeAssistant/ConfigDescription.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import {
|
||||||
|
INodeProperties
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const configOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'config',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Get the configuration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Check Configuration',
|
||||||
|
value: 'check',
|
||||||
|
description: 'Check the configuration',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'get',
|
||||||
|
description: 'The operation to perform.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
144
packages/nodes-base/nodes/HomeAssistant/EventDescription.ts
Normal file
144
packages/nodes-base/nodes/HomeAssistant/EventDescription.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import {
|
||||||
|
INodeProperties
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const eventOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'event',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Get all events',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Post',
|
||||||
|
value: 'post',
|
||||||
|
description: 'Post an event',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
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: 100,
|
||||||
|
},
|
||||||
|
default: 50,
|
||||||
|
description: 'How many results to return.',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* event:post */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Event Type',
|
||||||
|
name: 'eventType',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'post',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'event',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
description: 'The Entity ID for which an event will be created.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Event Attributes',
|
||||||
|
name: 'eventAttributes',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Attribute',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'event',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'post',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Attributes',
|
||||||
|
name: 'attributes',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Name of the attribute.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Value of the attribute.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
42
packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts
Normal file
42
packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import {
|
||||||
|
OptionsWithUri
|
||||||
|
} from 'request';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
NodeApiError,
|
||||||
|
NodeOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function homeAssistantApiRequest(this: IExecuteFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) {
|
||||||
|
const credentials = this.getCredentials('homeAssistantApi');
|
||||||
|
|
||||||
|
if (credentials === undefined) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let options: OptionsWithUri = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${credentials.accessToken}`,
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
qs,
|
||||||
|
body,
|
||||||
|
uri: uri ?? `${credentials.ssl === true ? 'https' : 'http'}://${credentials.host}:${credentials.port}/api${resource}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
options = Object.assign({}, options, option);
|
||||||
|
if (Object.keys(options.body).length === 0) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.helpers.request(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
128
packages/nodes-base/nodes/HomeAssistant/HistoryDescription.ts
Normal file
128
packages/nodes-base/nodes/HomeAssistant/HistoryDescription.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import {
|
||||||
|
INodeProperties
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const historyOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'history',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Get all state changes',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
description: 'The operation to perform.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const historyFields = [
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* history:getLogbookEntries */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'history',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: [
|
||||||
|
'history',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 100,
|
||||||
|
},
|
||||||
|
default: 50,
|
||||||
|
description: 'How many results to return.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'history',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'End Time',
|
||||||
|
name: 'endTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'The end of the period.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Entity IDs',
|
||||||
|
name: 'entityIds',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The entities IDs separated by comma.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Minimal Response',
|
||||||
|
name: 'minimalResponse',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'To only return <code>last_changed</code> and state for states.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Significant Changes Only',
|
||||||
|
name: 'significantChangesOnly',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Only return significant state changes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Start Time',
|
||||||
|
name: 'startTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'The beginning of the period.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.homeAssistant",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Miscellaneous"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/credentials/homeAssistant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.homeAssistant/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
377
packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts
Normal file
377
packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
configOperations,
|
||||||
|
} from './ConfigDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
serviceFields,
|
||||||
|
serviceOperations,
|
||||||
|
} from './ServiceDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
stateFields,
|
||||||
|
stateOperations,
|
||||||
|
} from './StateDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
eventFields,
|
||||||
|
eventOperations,
|
||||||
|
} from './EventDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
logFields,
|
||||||
|
logOperations,
|
||||||
|
} from './LogDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
templateFields,
|
||||||
|
templateOperations,
|
||||||
|
} from './TemplateDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
historyFields,
|
||||||
|
historyOperations,
|
||||||
|
} from './HistoryDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
cameraProxyFields,
|
||||||
|
cameraProxyOperations,
|
||||||
|
} from './CameraProxyDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
homeAssistantApiRequest,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
export class HomeAssistant implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Home Assistant',
|
||||||
|
name: 'homeAssistant',
|
||||||
|
icon: 'file:homeAssistant.svg',
|
||||||
|
group: ['output'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
|
description: 'Consume Home Assistant API',
|
||||||
|
defaults: {
|
||||||
|
name: 'Home Assistant',
|
||||||
|
color: '#3578e5',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'homeAssistantApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Camera Proxy',
|
||||||
|
value: 'cameraProxy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Config',
|
||||||
|
value: 'config',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'Event',
|
||||||
|
// value: 'event',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'History',
|
||||||
|
// value: 'history',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
name: 'Log',
|
||||||
|
value: 'log',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Service',
|
||||||
|
value: 'service',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'State',
|
||||||
|
value: 'state',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Template',
|
||||||
|
value: 'template',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'config',
|
||||||
|
description: 'Resource to consume.',
|
||||||
|
},
|
||||||
|
...cameraProxyOperations,
|
||||||
|
...cameraProxyFields,
|
||||||
|
...configOperations,
|
||||||
|
...eventOperations,
|
||||||
|
...eventFields,
|
||||||
|
...historyOperations,
|
||||||
|
...historyFields,
|
||||||
|
...logOperations,
|
||||||
|
...logFields,
|
||||||
|
...serviceOperations,
|
||||||
|
...serviceFields,
|
||||||
|
...stateOperations,
|
||||||
|
...stateFields,
|
||||||
|
...templateOperations,
|
||||||
|
...templateFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
const length = items.length;
|
||||||
|
const resource = this.getNodeParameter('resource', 0) as string;
|
||||||
|
const operation = this.getNodeParameter('operation', 0) as string;
|
||||||
|
const qs: IDataObject = {};
|
||||||
|
let responseData;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
try {
|
||||||
|
if (resource === 'config') {
|
||||||
|
if (operation === 'get') {
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', '/config');
|
||||||
|
} else if (operation === 'check') {
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'POST', '/config/core/check_config');
|
||||||
|
}
|
||||||
|
} else if (resource === 'service') {
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', '/services') as IDataObject[];
|
||||||
|
if (!returnAll) {
|
||||||
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
|
responseData = responseData.slice(0, limit);
|
||||||
|
}
|
||||||
|
} else if (operation === 'call') {
|
||||||
|
const domain = this.getNodeParameter('domain', i) as string;
|
||||||
|
const service = this.getNodeParameter('service', i) as string;
|
||||||
|
const serviceAttributes = this.getNodeParameter('serviceAttributes', i) as {
|
||||||
|
attributes: IDataObject[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const body: IDataObject = {};
|
||||||
|
|
||||||
|
if (Object.entries(serviceAttributes).length) {
|
||||||
|
if (serviceAttributes.attributes !== undefined) {
|
||||||
|
serviceAttributes.attributes.map(
|
||||||
|
attribute => {
|
||||||
|
// @ts-ignore
|
||||||
|
body[attribute.name as string] = attribute.value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'POST', `/services/${domain}/${service}`, body);
|
||||||
|
if (Array.isArray(responseData) && responseData.length === 0) {
|
||||||
|
responseData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (resource === 'state') {
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', '/states') as IDataObject[];
|
||||||
|
if (!returnAll) {
|
||||||
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
|
responseData = responseData.slice(0, limit);
|
||||||
|
}
|
||||||
|
} else if (operation === 'get') {
|
||||||
|
const entityId = this.getNodeParameter('entityId', i) as string;
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', `/states/${entityId}`);
|
||||||
|
} else if (operation === 'upsert') {
|
||||||
|
const entityId = this.getNodeParameter('entityId', i) as string;
|
||||||
|
const state = this.getNodeParameter('state', i) as string;
|
||||||
|
const stateAttributes = this.getNodeParameter('stateAttributes', i) as {
|
||||||
|
attributes: IDataObject[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
state,
|
||||||
|
attributes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.entries(stateAttributes).length) {
|
||||||
|
if (stateAttributes.attributes !== undefined) {
|
||||||
|
stateAttributes.attributes.map(
|
||||||
|
attribute => {
|
||||||
|
// @ts-ignore
|
||||||
|
body.attributes[attribute.name as string] = attribute.value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'POST', `/states/${entityId}`, body);
|
||||||
|
}
|
||||||
|
} else if (resource === 'event') {
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', '/events') as IDataObject[];
|
||||||
|
if (!returnAll) {
|
||||||
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
|
responseData = responseData.slice(0, limit);
|
||||||
|
}
|
||||||
|
} else if (operation === 'post') {
|
||||||
|
const eventType = this.getNodeParameter('eventType', i) as string;
|
||||||
|
const eventAttributes = this.getNodeParameter('eventAttributes', i) as {
|
||||||
|
attributes: IDataObject[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = {};
|
||||||
|
|
||||||
|
if (Object.entries(eventAttributes).length) {
|
||||||
|
if (eventAttributes.attributes !== undefined) {
|
||||||
|
eventAttributes.attributes.map(
|
||||||
|
attribute => {
|
||||||
|
// @ts-ignore
|
||||||
|
body[attribute.name as string] = attribute.value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'POST', `/events/${eventType}`, body);
|
||||||
|
|
||||||
|
}
|
||||||
|
} else if (resource === 'log') {
|
||||||
|
if (operation === 'getErroLogs') {
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', '/error_log');
|
||||||
|
if (responseData) {
|
||||||
|
responseData = {
|
||||||
|
errorLog: responseData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (operation === 'getLogbookEntries') {
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||||
|
let endpoint = '/logbook';
|
||||||
|
|
||||||
|
if (Object.entries(additionalFields).length) {
|
||||||
|
if (additionalFields.startTime) {
|
||||||
|
endpoint = `/logbook/${additionalFields.startTime}`;
|
||||||
|
}
|
||||||
|
if (additionalFields.endTime) {
|
||||||
|
qs.end_time = additionalFields.endTime;
|
||||||
|
}
|
||||||
|
if (additionalFields.entityId) {
|
||||||
|
qs.entity = additionalFields.entityId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', endpoint, {}, qs);
|
||||||
|
|
||||||
|
}
|
||||||
|
} else if (resource === 'template') {
|
||||||
|
if (operation === 'create') {
|
||||||
|
const body = {
|
||||||
|
template: this.getNodeParameter('template', i) as string,
|
||||||
|
};
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'POST', '/template', body);
|
||||||
|
if (responseData) {
|
||||||
|
responseData = { renderedTemplate: responseData };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (resource === 'history') {
|
||||||
|
if (operation === 'getAll') {
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||||
|
let endpoint = '/history/period';
|
||||||
|
|
||||||
|
if (Object.entries(additionalFields).length) {
|
||||||
|
if (additionalFields.startTime) {
|
||||||
|
endpoint = `/history/period/${additionalFields.startTime}`;
|
||||||
|
}
|
||||||
|
if (additionalFields.endTime) {
|
||||||
|
qs.end_time = additionalFields.endTime;
|
||||||
|
}
|
||||||
|
if (additionalFields.entityIds) {
|
||||||
|
qs.filter_entity_id = additionalFields.entityIds;
|
||||||
|
}
|
||||||
|
if (additionalFields.minimalResponse === true) {
|
||||||
|
qs.minimal_response = additionalFields.minimalResponse;
|
||||||
|
}
|
||||||
|
if (additionalFields.significantChangesOnly === true) {
|
||||||
|
qs.significant_changes_only = additionalFields.significantChangesOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', endpoint, {}, qs) as IDataObject[];
|
||||||
|
if (!returnAll) {
|
||||||
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
|
responseData = responseData.slice(0, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (resource === 'cameraProxy') {
|
||||||
|
if (operation === 'getScreenshot') {
|
||||||
|
const cameraEntityId = this.getNodeParameter('cameraEntityId', i) as string;
|
||||||
|
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||||
|
const endpoint = `/camera_proxy/${cameraEntityId}`;
|
||||||
|
|
||||||
|
let mimeType: string | undefined;
|
||||||
|
|
||||||
|
responseData = await homeAssistantApiRequest.call(this, 'GET', endpoint, {}, {}, undefined, {
|
||||||
|
encoding: null,
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newItem: INodeExecutionData = {
|
||||||
|
json: items[i].json,
|
||||||
|
binary: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mimeType === undefined && responseData.headers['content-type']) {
|
||||||
|
mimeType = responseData.headers['content-type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items[i].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, items[i].binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i] = newItem;
|
||||||
|
|
||||||
|
const data = Buffer.from(responseData.body as string);
|
||||||
|
|
||||||
|
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data, 'screenshot.jpg', mimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
if (resource === 'cameraProxy' && operation === 'get') {
|
||||||
|
items[i].json = { error: error.message };
|
||||||
|
} else {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.isArray(responseData)
|
||||||
|
? returnData.push(...responseData)
|
||||||
|
: returnData.push(responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource === 'cameraProxy' && operation === 'getScreenshot') {
|
||||||
|
return this.prepareOutputData(items);
|
||||||
|
} else {
|
||||||
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
packages/nodes-base/nodes/HomeAssistant/LogDescription.ts
Normal file
78
packages/nodes-base/nodes/HomeAssistant/LogDescription.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import {
|
||||||
|
INodeProperties
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const logOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get Error Logs',
|
||||||
|
value: 'getErroLogs',
|
||||||
|
description: 'Get a log for a specific entity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get Logbook Entries',
|
||||||
|
value: 'getLogbookEntries',
|
||||||
|
description: 'Get all logs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getErroLogs',
|
||||||
|
description: 'The operation to perform.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const logFields = [
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* log:getLogbookEntries */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'getLogbookEntries',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'End Time',
|
||||||
|
name: 'endTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'The end of the period.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Entity ID',
|
||||||
|
name: 'entityId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The entity ID.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Start Time',
|
||||||
|
name: 'startTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'The beginning of the period.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
159
packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts
Normal file
159
packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import {
|
||||||
|
INodeProperties
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const serviceOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'service',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Call',
|
||||||
|
value: 'call',
|
||||||
|
description: 'Call a service within a specific domain',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Get all services',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
description: 'The operation to perform.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const serviceFields = [
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* service:getAll */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'service',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: [
|
||||||
|
'service',
|
||||||
|
],
|
||||||
|
returnAll: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 100,
|
||||||
|
},
|
||||||
|
default: 50,
|
||||||
|
description: 'How many results to return.',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* service:Call */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Domain',
|
||||||
|
name: 'domain',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'service',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'call',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Service',
|
||||||
|
name: 'service',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'service',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'call',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Service Attributes',
|
||||||
|
name: 'serviceAttributes',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Attribute',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'service',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'call',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'attributes',
|
||||||
|
displayName: 'Attributes',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Name of the field.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Value of the field.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue