mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-03 17:07:29 -08:00
Merge branch 'master' of github.com:n8n-io/n8n into n8n-2349-connectors
This commit is contained in:
commit
57dc5ca761
|
@ -1,3 +1,4 @@
|
||||||
packages/nodes-base
|
packages/nodes-base
|
||||||
packages/editor-ui
|
packages/editor-ui
|
||||||
packages/design-system
|
packages/design-system
|
||||||
|
*package.json
|
||||||
|
|
|
@ -49,6 +49,10 @@ dependencies are installed and the packages get linked correctly. Here a short g
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
|
#### Node.js
|
||||||
|
|
||||||
|
We suggest using the current [Node.js](https://nodejs.org/en/) LTS version (14.18.0 which includes npm 6.14.15) for development purposes.
|
||||||
|
|
||||||
#### Build tools
|
#### Build tools
|
||||||
|
|
||||||
The packages which n8n uses depend on a few build tools:
|
The packages which n8n uses depend on a few build tools:
|
||||||
|
|
61
packages/cli/commands/db/revert.ts
Normal file
61
packages/cli/commands/db/revert.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import { Command, flags } from '@oclif/command';
|
||||||
|
import { Connection, ConnectionOptions, createConnection } from 'typeorm';
|
||||||
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { getLogger } from '../../src/Logger';
|
||||||
|
|
||||||
|
import { Db } from '../../src';
|
||||||
|
|
||||||
|
export class DbRevertMigrationCommand extends Command {
|
||||||
|
static description = 'Revert last database migration';
|
||||||
|
|
||||||
|
static examples = ['$ n8n db:revert'];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
help: flags.help({ char: 'h' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow, @typescript-eslint/no-unused-vars
|
||||||
|
const { flags } = this.parse(DbRevertMigrationCommand);
|
||||||
|
|
||||||
|
let connection: Connection | undefined;
|
||||||
|
try {
|
||||||
|
await Db.init();
|
||||||
|
connection = Db.collections.Credentials?.manager.connection;
|
||||||
|
|
||||||
|
if (!connection) {
|
||||||
|
throw new Error(`No database connection available.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionOptions: ConnectionOptions = Object.assign(connection.options, {
|
||||||
|
subscribers: [],
|
||||||
|
synchronize: false,
|
||||||
|
migrationsRun: false,
|
||||||
|
dropSchema: false,
|
||||||
|
logging: ['query', 'error', 'schema'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// close connection in order to reconnect with updated options
|
||||||
|
await connection.close();
|
||||||
|
connection = await createConnection(connectionOptions);
|
||||||
|
|
||||||
|
await connection.undoLastMigration();
|
||||||
|
await connection.close();
|
||||||
|
} catch (error) {
|
||||||
|
if (connection) await connection.close();
|
||||||
|
|
||||||
|
console.error('Error reverting last migration. See log messages for details.');
|
||||||
|
logger.error(error.message);
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,7 +129,8 @@ export class ExportCredentialsCommand extends Command {
|
||||||
|
|
||||||
for (let i = 0; i < credentials.length; i++) {
|
for (let i = 0; i < credentials.length; i++) {
|
||||||
const { name, type, nodesAccess, data } = credentials[i];
|
const { name, type, nodesAccess, data } = credentials[i];
|
||||||
const credential = new Credentials(name, type, nodesAccess, data);
|
const id = credentials[i].id as string;
|
||||||
|
const credential = new Credentials({ id, name }, type, nodesAccess, data);
|
||||||
const plainData = credential.getData(encryptionKey);
|
const plainData = credential.getData(encryptionKey);
|
||||||
(credentials[i] as ICredentialsDecryptedDb).data = plainData;
|
(credentials[i] as ICredentialsDecryptedDb).data = plainData;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
|
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { INode, INodeCredentialsDetails, LoggerProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as glob from 'fast-glob';
|
import * as glob from 'fast-glob';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { UserSettings } from 'n8n-core';
|
||||||
import { getLogger } from '../../src/Logger';
|
import { getLogger } from '../../src/Logger';
|
||||||
import { Db } from '../../src';
|
import { Db, ICredentialsDb } from '../../src';
|
||||||
|
|
||||||
export class ImportWorkflowsCommand extends Command {
|
export class ImportWorkflowsCommand extends Command {
|
||||||
static description = 'Import workflows';
|
static description = 'Import workflows';
|
||||||
|
@ -30,6 +30,32 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private transformCredentials(node: INode, credentialsEntities: ICredentialsDb[]) {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
const nodeCredentials: INodeCredentialsDetails = {
|
||||||
|
id: null,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchingCredentials = credentialsEntities.filter(
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingCredentials.length === 1) {
|
||||||
|
nodeCredentials.id = matchingCredentials[0].id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
node.credentials[type] = nodeCredentials;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
|
@ -57,6 +83,7 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
|
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
await UserSettings.prepareUserSettings();
|
await UserSettings.prepareUserSettings();
|
||||||
|
const credentialsEntities = (await Db.collections.Credentials?.find()) ?? [];
|
||||||
let i;
|
let i;
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
const files = await glob(
|
const files = await glob(
|
||||||
|
@ -64,6 +91,12 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
);
|
);
|
||||||
for (i = 0; i < files.length; i++) {
|
for (i = 0; i < files.length; i++) {
|
||||||
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
||||||
|
if (credentialsEntities.length > 0) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
workflow.nodes.forEach((node: INode) => {
|
||||||
|
this.transformCredentials(node, credentialsEntities);
|
||||||
|
});
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Workflow!.save(workflow);
|
await Db.collections.Workflow!.save(workflow);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +108,12 @@ export class ImportWorkflowsCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < fileContents.length; i++) {
|
for (i = 0; i < fileContents.length; i++) {
|
||||||
|
if (credentialsEntities.length > 0) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
fileContents[i].nodes.forEach((node: INode) => {
|
||||||
|
this.transformCredentials(node, credentialsEntities);
|
||||||
|
});
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
await Db.collections.Workflow!.save(fileContents[i]);
|
await Db.collections.Workflow!.save(fileContents[i]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { getLogger } from '../src/Logger';
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
|
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
||||||
let processExistCode = 0;
|
let processExitCode = 0;
|
||||||
|
|
||||||
export class Start extends Command {
|
export class Start extends Command {
|
||||||
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
|
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
|
||||||
|
@ -92,7 +92,7 @@ export class Start extends Command {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// In case that something goes wrong with shutdown we
|
// In case that something goes wrong with shutdown we
|
||||||
// kill after max. 30 seconds no matter what
|
// kill after max. 30 seconds no matter what
|
||||||
process.exit(processExistCode);
|
process.exit(processExitCode);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
const skipWebhookDeregistration = config.get(
|
const skipWebhookDeregistration = config.get(
|
||||||
|
@ -133,7 +133,7 @@ export class Start extends Command {
|
||||||
console.error('There was an error shutting down n8n.', error);
|
console.error('There was an error shutting down n8n.', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(processExistCode);
|
process.exit(processExitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
@ -160,7 +160,7 @@ export class Start extends Command {
|
||||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExitCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -355,7 +355,7 @@ export class Start extends Command {
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
this.error(`There was an error: ${error.message}`);
|
this.error(`There was an error: ${error.message}`);
|
||||||
|
|
||||||
processExistCode = 1;
|
processExitCode = 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.emit('SIGINT');
|
process.emit('SIGINT');
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ module.exports = [
|
||||||
logging: true,
|
logging: true,
|
||||||
entities: Object.values(entities),
|
entities: Object.values(entities),
|
||||||
database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
|
database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
|
||||||
migrations: ['./src/databases/sqlite/migrations/*.ts'],
|
migrations: ['./src/databases/sqlite/migrations/index.ts'],
|
||||||
subscribers: ['./src/databases/sqlite/subscribers/*.ts'],
|
subscribers: ['./src/databases/sqlite/subscribers/*.ts'],
|
||||||
cli: {
|
cli: {
|
||||||
entitiesDir: './src/databases/entities',
|
entitiesDir: './src/databases/entities',
|
||||||
|
@ -28,7 +28,7 @@ module.exports = [
|
||||||
database: 'n8n',
|
database: 'n8n',
|
||||||
schema: 'public',
|
schema: 'public',
|
||||||
entities: Object.values(entities),
|
entities: Object.values(entities),
|
||||||
migrations: ['./src/databases/postgresdb/migrations/*.ts'],
|
migrations: ['./src/databases/postgresdb/migrations/index.ts'],
|
||||||
subscribers: ['src/subscriber/**/*.ts'],
|
subscribers: ['src/subscriber/**/*.ts'],
|
||||||
cli: {
|
cli: {
|
||||||
entitiesDir: './src/databases/entities',
|
entitiesDir: './src/databases/entities',
|
||||||
|
@ -46,7 +46,7 @@ module.exports = [
|
||||||
port: '3306',
|
port: '3306',
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: Object.values(entities),
|
entities: Object.values(entities),
|
||||||
migrations: ['./src/databases/mysqldb/migrations/*.ts'],
|
migrations: ['./src/databases/mysqldb/migrations/index.ts'],
|
||||||
subscribers: ['src/subscriber/**/*.ts'],
|
subscribers: ['src/subscriber/**/*.ts'],
|
||||||
cli: {
|
cli: {
|
||||||
entitiesDir: './src/databases/entities',
|
entitiesDir: './src/databases/entities',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.142.0",
|
"version": "0.144.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",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"start:windows": "cd bin && n8n",
|
"start:windows": "cd bin && n8n",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
"typeorm": "ts-node ../../node_modules/typeorm/cli.js"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"n8n": "./bin/n8n"
|
"n8n": "./bin/n8n"
|
||||||
|
@ -109,10 +109,10 @@
|
||||||
"localtunnel": "^2.0.0",
|
"localtunnel": "^2.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mysql2": "~2.3.0",
|
"mysql2": "~2.3.0",
|
||||||
"n8n-core": "~0.87.0",
|
"n8n-core": "~0.89.0",
|
||||||
"n8n-editor-ui": "~0.110.0",
|
"n8n-editor-ui": "~0.112.0",
|
||||||
"n8n-nodes-base": "~0.139.0",
|
"n8n-nodes-base": "~0.141.0",
|
||||||
"n8n-workflow": "~0.71.0",
|
"n8n-workflow": "~0.72.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",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ICredentialsExpressionResolveValues,
|
ICredentialsExpressionResolveValues,
|
||||||
ICredentialsHelper,
|
ICredentialsHelper,
|
||||||
INode,
|
INode,
|
||||||
|
INodeCredentialsDetails,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodeType,
|
INodeType,
|
||||||
|
@ -39,30 +40,32 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
/**
|
/**
|
||||||
* Returns the credentials instance
|
* Returns the credentials instance
|
||||||
*
|
*
|
||||||
* @param {string} name Name of the credentials to return instance of
|
* @param {INodeCredentialsDetails} nodeCredentials id and name to return instance of
|
||||||
* @param {string} type Type of the credentials to return instance of
|
* @param {string} type Type of the credentials to return instance of
|
||||||
* @returns {Credentials}
|
* @returns {Credentials}
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
async getCredentials(name: string, type: string): Promise<Credentials> {
|
async getCredentials(
|
||||||
const credentialsDb = await Db.collections.Credentials?.find({ type });
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
|
type: string,
|
||||||
if (credentialsDb === undefined || credentialsDb.length === 0) {
|
): Promise<Credentials> {
|
||||||
throw new Error(`No credentials of type "${type}" exist.`);
|
if (!nodeCredentials.id) {
|
||||||
|
throw new Error(`Credentials "${nodeCredentials.name}" for type "${type}" don't have an ID.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
const credentials = await Db.collections.Credentials?.findOne({ id: nodeCredentials.id, type });
|
||||||
const credential = credentialsDb.find((credential) => credential.name === name);
|
|
||||||
|
|
||||||
if (credential === undefined) {
|
if (!credentials) {
|
||||||
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
|
throw new Error(
|
||||||
|
`Credentials with ID "${nodeCredentials.id}" don't exist for type "${type}".`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Credentials(
|
return new Credentials(
|
||||||
credential.name,
|
{ id: credentials.id.toString(), name: credentials.name },
|
||||||
credential.type,
|
credentials.type,
|
||||||
credential.nodesAccess,
|
credentials.nodesAccess,
|
||||||
credential.data,
|
credentials.data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,21 +104,20 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
/**
|
/**
|
||||||
* Returns the decrypted credential data with applied overwrites
|
* Returns the decrypted credential data with applied overwrites
|
||||||
*
|
*
|
||||||
* @param {string} name Name of the credentials to return data of
|
* @param {INodeCredentialsDetails} nodeCredentials id and name to return instance of
|
||||||
* @param {string} type Type of the credentials to return data of
|
* @param {string} type Type of the credentials to return data of
|
||||||
* @param {boolean} [raw] Return the data as supplied without defaults or overwrites
|
* @param {boolean} [raw] Return the data as supplied without defaults or overwrites
|
||||||
* @returns {ICredentialDataDecryptedObject}
|
* @returns {ICredentialDataDecryptedObject}
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
async getDecrypted(
|
async getDecrypted(
|
||||||
name: string,
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
type: string,
|
type: string,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
raw?: boolean,
|
raw?: boolean,
|
||||||
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
||||||
): Promise<ICredentialDataDecryptedObject> {
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
const credentials = await this.getCredentials(name, type);
|
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||||
|
|
||||||
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
||||||
|
|
||||||
if (raw === true) {
|
if (raw === true) {
|
||||||
|
@ -228,12 +230,12 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
* @memberof CredentialsHelper
|
* @memberof CredentialsHelper
|
||||||
*/
|
*/
|
||||||
async updateCredentials(
|
async updateCredentials(
|
||||||
name: string,
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
type: string,
|
type: string,
|
||||||
data: ICredentialDataDecryptedObject,
|
data: ICredentialDataDecryptedObject,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||||
const credentials = await this.getCredentials(name, type);
|
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||||
|
|
||||||
if (Db.collections.Credentials === null) {
|
if (Db.collections.Credentials === null) {
|
||||||
// The first time executeWorkflow gets called the Database has
|
// The first time executeWorkflow gets called the Database has
|
||||||
|
@ -251,7 +253,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
|
|
||||||
// Save the credentials in DB
|
// Save the credentials in DB
|
||||||
const findQuery = {
|
const findQuery = {
|
||||||
name,
|
id: credentials.id,
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import { join as pathJoin } from 'path';
|
import { join as pathJoin } from 'path';
|
||||||
import { readFile as fsReadFile } from 'fs/promises';
|
import { readFile as fsReadFile } from 'fs/promises';
|
||||||
import { readFileSync as fsReadFileSync } from 'fs';
|
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
|
@ -137,45 +136,6 @@ export async function getConfigValue(
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets value from config with support for "_FILE" environment variables synchronously
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {string} configKey The key of the config data to get
|
|
||||||
* @returns {(string | boolean | number | undefined)}
|
|
||||||
*/
|
|
||||||
export function getConfigValueSync(configKey: string): string | boolean | number | undefined {
|
|
||||||
// Get the environment variable
|
|
||||||
const configSchema = config.getSchema();
|
|
||||||
// @ts-ignore
|
|
||||||
const currentSchema = extractSchemaForKey(configKey, configSchema._cvtProperties as IDataObject);
|
|
||||||
// Check if environment variable is defined for config key
|
|
||||||
if (currentSchema.env === undefined) {
|
|
||||||
// No environment variable defined, so return value from config
|
|
||||||
return config.get(configKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if special file enviroment variable exists
|
|
||||||
const fileEnvironmentVariable = process.env[`${currentSchema.env}_FILE`];
|
|
||||||
if (fileEnvironmentVariable === undefined) {
|
|
||||||
// Does not exist, so return value from config
|
|
||||||
return config.get(configKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = fsReadFileSync(fileEnvironmentVariable, 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a unique name for a workflow or credentials entity.
|
* Generate a unique name for a workflow or credentials entity.
|
||||||
*
|
*
|
||||||
|
|
|
@ -66,6 +66,7 @@ import {
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeCredentials,
|
INodeCredentials,
|
||||||
|
INodeCredentialsDetails,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodePropertyOptions,
|
INodePropertyOptions,
|
||||||
INodeType,
|
INodeType,
|
||||||
|
@ -642,6 +643,9 @@ class App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check credentials for old format
|
||||||
|
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
|
||||||
|
|
||||||
await this.externalHooks.run('workflow.create', [newWorkflow]);
|
await this.externalHooks.run('workflow.create', [newWorkflow]);
|
||||||
|
|
||||||
await WorkflowHelpers.validateWorkflow(newWorkflow);
|
await WorkflowHelpers.validateWorkflow(newWorkflow);
|
||||||
|
@ -782,6 +786,9 @@ class App {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
updateData.id = id;
|
updateData.id = id;
|
||||||
|
|
||||||
|
// check credentials for old format
|
||||||
|
await WorkflowHelpers.replaceInvalidCredentials(updateData as WorkflowEntity);
|
||||||
|
|
||||||
await this.externalHooks.run('workflow.update', [updateData]);
|
await this.externalHooks.run('workflow.update', [updateData]);
|
||||||
|
|
||||||
const isActive = await this.activeWorkflowRunner.isActive(id);
|
const isActive = await this.activeWorkflowRunner.isActive(id);
|
||||||
|
@ -1293,26 +1300,9 @@ class App {
|
||||||
throw new Error('Credentials have to have a name set!');
|
throw new Error('Credentials have to have a name set!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if credentials with the same name and type exist already
|
|
||||||
const findQuery = {
|
|
||||||
where: {
|
|
||||||
name: incomingData.name,
|
|
||||||
type: incomingData.type,
|
|
||||||
},
|
|
||||||
} as FindOneOptions;
|
|
||||||
|
|
||||||
const checkResult = await Db.collections.Credentials!.findOne(findQuery);
|
|
||||||
if (checkResult !== undefined) {
|
|
||||||
throw new ResponseHelper.ResponseError(
|
|
||||||
`Credentials with the same type and name exist already.`,
|
|
||||||
undefined,
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data
|
||||||
const credentials = new Credentials(
|
const credentials = new Credentials(
|
||||||
incomingData.name,
|
{ id: null, name: incomingData.name },
|
||||||
incomingData.type,
|
incomingData.type,
|
||||||
incomingData.nodesAccess,
|
incomingData.nodesAccess,
|
||||||
);
|
);
|
||||||
|
@ -1321,10 +1311,6 @@ class App {
|
||||||
|
|
||||||
await this.externalHooks.run('credentials.create', [newCredentialsData]);
|
await this.externalHooks.run('credentials.create', [newCredentialsData]);
|
||||||
|
|
||||||
// Add special database related data
|
|
||||||
|
|
||||||
// TODO: also add user automatically depending on who is logged in, if anybody is logged in
|
|
||||||
|
|
||||||
// Save the credentials in DB
|
// Save the credentials in DB
|
||||||
const result = await Db.collections.Credentials!.save(newCredentialsData);
|
const result = await Db.collections.Credentials!.save(newCredentialsData);
|
||||||
result.data = incomingData.data;
|
result.data = incomingData.data;
|
||||||
|
@ -1445,24 +1431,6 @@ class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if credentials with the same name and type exist already
|
|
||||||
const findQuery = {
|
|
||||||
where: {
|
|
||||||
id: Not(id),
|
|
||||||
name: incomingData.name,
|
|
||||||
type: incomingData.type,
|
|
||||||
},
|
|
||||||
} as FindOneOptions;
|
|
||||||
|
|
||||||
const checkResult = await Db.collections.Credentials!.findOne(findQuery);
|
|
||||||
if (checkResult !== undefined) {
|
|
||||||
throw new ResponseHelper.ResponseError(
|
|
||||||
`Credentials with the same type and name exist already.`,
|
|
||||||
undefined,
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
if (encryptionKey === undefined) {
|
if (encryptionKey === undefined) {
|
||||||
throw new Error('No encryption key got found to encrypt the credentials!');
|
throw new Error('No encryption key got found to encrypt the credentials!');
|
||||||
|
@ -1479,7 +1447,7 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentlySavedCredentials = new Credentials(
|
const currentlySavedCredentials = new Credentials(
|
||||||
result.name,
|
result as INodeCredentialsDetails,
|
||||||
result.type,
|
result.type,
|
||||||
result.nodesAccess,
|
result.nodesAccess,
|
||||||
result.data,
|
result.data,
|
||||||
|
@ -1494,7 +1462,7 @@ class App {
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data
|
||||||
const credentials = new Credentials(
|
const credentials = new Credentials(
|
||||||
incomingData.name,
|
{ id, name: incomingData.name },
|
||||||
incomingData.type,
|
incomingData.type,
|
||||||
incomingData.nodesAccess,
|
incomingData.nodesAccess,
|
||||||
);
|
);
|
||||||
|
@ -1563,7 +1531,7 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = new Credentials(
|
const credentials = new Credentials(
|
||||||
result.name,
|
result as INodeCredentialsDetails,
|
||||||
result.type,
|
result.type,
|
||||||
result.nodesAccess,
|
result.nodesAccess,
|
||||||
result.data,
|
result.data,
|
||||||
|
@ -1707,7 +1675,7 @@ class App {
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
result.name,
|
result as INodeCredentialsDetails,
|
||||||
result.type,
|
result.type,
|
||||||
mode,
|
mode,
|
||||||
true,
|
true,
|
||||||
|
@ -1766,7 +1734,11 @@ class App {
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data
|
||||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
const credentials = new Credentials(
|
||||||
|
result as INodeCredentialsDetails,
|
||||||
|
result.type,
|
||||||
|
result.nodesAccess,
|
||||||
|
);
|
||||||
|
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
|
@ -1823,13 +1795,13 @@ class App {
|
||||||
// Decrypt the currently saved credentials
|
// Decrypt the currently saved credentials
|
||||||
const workflowCredentials: IWorkflowCredentials = {
|
const workflowCredentials: IWorkflowCredentials = {
|
||||||
[result.type]: {
|
[result.type]: {
|
||||||
[result.name]: result as ICredentialsEncrypted,
|
[result.id.toString()]: result as ICredentialsEncrypted,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
result.name,
|
result as INodeCredentialsDetails,
|
||||||
result.type,
|
result.type,
|
||||||
mode,
|
mode,
|
||||||
true,
|
true,
|
||||||
|
@ -1868,7 +1840,11 @@ class App {
|
||||||
|
|
||||||
decryptedDataOriginal.oauthTokenData = oauthTokenJson;
|
decryptedDataOriginal.oauthTokenData = oauthTokenJson;
|
||||||
|
|
||||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
const credentials = new Credentials(
|
||||||
|
result as INodeCredentialsDetails,
|
||||||
|
result.type,
|
||||||
|
result.nodesAccess,
|
||||||
|
);
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
|
@ -1913,7 +1889,7 @@ class App {
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
result.name,
|
result as INodeCredentialsDetails,
|
||||||
result.type,
|
result.type,
|
||||||
mode,
|
mode,
|
||||||
true,
|
true,
|
||||||
|
@ -1950,7 +1926,11 @@ class App {
|
||||||
const oAuthObj = new clientOAuth2(oAuthOptions);
|
const oAuthObj = new clientOAuth2(oAuthOptions);
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data
|
||||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
const credentials = new Credentials(
|
||||||
|
result as INodeCredentialsDetails,
|
||||||
|
result.type,
|
||||||
|
result.nodesAccess,
|
||||||
|
);
|
||||||
decryptedDataOriginal.csrfSecret = csrfSecret;
|
decryptedDataOriginal.csrfSecret = csrfSecret;
|
||||||
|
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||||
|
@ -2039,14 +2019,14 @@ class App {
|
||||||
// Decrypt the currently saved credentials
|
// Decrypt the currently saved credentials
|
||||||
const workflowCredentials: IWorkflowCredentials = {
|
const workflowCredentials: IWorkflowCredentials = {
|
||||||
[result.type]: {
|
[result.type]: {
|
||||||
[result.name]: result as ICredentialsEncrypted,
|
[result.id.toString()]: result as ICredentialsEncrypted,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
result.name,
|
result as INodeCredentialsDetails,
|
||||||
result.type,
|
result.type,
|
||||||
mode,
|
mode,
|
||||||
true,
|
true,
|
||||||
|
@ -2128,7 +2108,11 @@ class App {
|
||||||
|
|
||||||
_.unset(decryptedDataOriginal, 'csrfSecret');
|
_.unset(decryptedDataOriginal, 'csrfSecret');
|
||||||
|
|
||||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
const credentials = new Credentials(
|
||||||
|
result as INodeCredentialsDetails,
|
||||||
|
result.type,
|
||||||
|
result.nodesAccess,
|
||||||
|
);
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
|
|
|
@ -10,7 +10,7 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
|
||||||
|
|
||||||
let node;
|
let node;
|
||||||
let type;
|
let type;
|
||||||
let name;
|
let nodeCredentials;
|
||||||
let foundCredentials;
|
let foundCredentials;
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (node of nodes) {
|
for (node of nodes) {
|
||||||
|
@ -21,19 +21,30 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (type of Object.keys(node.credentials)) {
|
for (type of Object.keys(node.credentials)) {
|
||||||
if (!returnCredentials.hasOwnProperty(type)) {
|
if (!returnCredentials[type]) {
|
||||||
returnCredentials[type] = {};
|
returnCredentials[type] = {};
|
||||||
}
|
}
|
||||||
name = node.credentials[type];
|
nodeCredentials = node.credentials[type];
|
||||||
|
|
||||||
if (!returnCredentials[type].hasOwnProperty(name)) {
|
if (!nodeCredentials.id) {
|
||||||
|
throw new Error(
|
||||||
|
`Credentials with name "${nodeCredentials.name}" for type "${type}" miss an ID.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!returnCredentials[type][nodeCredentials.id]) {
|
||||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||||
foundCredentials = await Db.collections.Credentials!.find({ name, type });
|
foundCredentials = await Db.collections.Credentials!.findOne({
|
||||||
if (!foundCredentials.length) {
|
id: nodeCredentials.id,
|
||||||
throw new Error(`Could not find credentials for type "${type}" with name "${name}".`);
|
type,
|
||||||
|
});
|
||||||
|
if (!foundCredentials) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find credentials for type "${type}" with ID "${nodeCredentials.id}".`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
returnCredentials[type][name] = foundCredentials[0];
|
returnCredentials[type][nodeCredentials.id] = foundCredentials;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
INode,
|
INode,
|
||||||
|
INodeCredentialsDetails,
|
||||||
IRun,
|
IRun,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
@ -385,6 +386,113 @@ export async function getStaticDataById(workflowId: string | number) {
|
||||||
return workflowData.staticData || {};
|
return workflowData.staticData || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checking if credentials of old format are in use and run a DB check if they might exist uniquely
|
||||||
|
export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promise<WorkflowEntity> {
|
||||||
|
const { nodes } = workflow;
|
||||||
|
if (!nodes) return workflow;
|
||||||
|
|
||||||
|
// caching
|
||||||
|
const credentialsByName: Record<string, Record<string, INodeCredentialsDetails>> = {};
|
||||||
|
const credentialsById: Record<string, Record<string, INodeCredentialsDetails>> = {};
|
||||||
|
|
||||||
|
// for loop to run DB fetches sequential and use cache to keep pressure off DB
|
||||||
|
// trade-off: longer response time for less DB queries
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (!node.credentials || node.disabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// extract credentials types
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [nodeCredentialType, nodeCredentials] of allNodeCredentials) {
|
||||||
|
// Check if Node applies old credentials style
|
||||||
|
if (typeof nodeCredentials === 'string' || nodeCredentials.id === null) {
|
||||||
|
const name = typeof nodeCredentials === 'string' ? nodeCredentials : nodeCredentials.name;
|
||||||
|
// init cache for type
|
||||||
|
if (!credentialsByName[nodeCredentialType]) {
|
||||||
|
credentialsByName[nodeCredentialType] = {};
|
||||||
|
}
|
||||||
|
if (credentialsByName[nodeCredentialType][name] === undefined) {
|
||||||
|
const credentials = await Db.collections.Credentials?.find({
|
||||||
|
name,
|
||||||
|
type: nodeCredentialType,
|
||||||
|
});
|
||||||
|
// if credential name-type combination is unique, use it
|
||||||
|
if (credentials?.length === 1) {
|
||||||
|
credentialsByName[nodeCredentialType][name] = {
|
||||||
|
id: credentials[0].id.toString(),
|
||||||
|
name: credentials[0].name,
|
||||||
|
};
|
||||||
|
node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing found - add invalid credentials to cache to prevent further DB checks
|
||||||
|
credentialsByName[nodeCredentialType][name] = {
|
||||||
|
id: null,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// get credentials from cache
|
||||||
|
node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node has credentials with an ID
|
||||||
|
|
||||||
|
// init cache for type
|
||||||
|
if (!credentialsById[nodeCredentialType]) {
|
||||||
|
credentialsById[nodeCredentialType] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if credentials for ID-type are not yet cached
|
||||||
|
if (credentialsById[nodeCredentialType][nodeCredentials.id] === undefined) {
|
||||||
|
// check first if ID-type combination exists
|
||||||
|
const credentials = await Db.collections.Credentials?.findOne({
|
||||||
|
id: nodeCredentials.id,
|
||||||
|
type: nodeCredentialType,
|
||||||
|
});
|
||||||
|
if (credentials) {
|
||||||
|
credentialsById[nodeCredentialType][nodeCredentials.id] = {
|
||||||
|
id: credentials.id.toString(),
|
||||||
|
name: credentials.name,
|
||||||
|
};
|
||||||
|
node.credentials[nodeCredentialType] =
|
||||||
|
credentialsById[nodeCredentialType][nodeCredentials.id];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// no credentials found for ID, check if some exist for name
|
||||||
|
const credsByName = await Db.collections.Credentials?.find({
|
||||||
|
name: nodeCredentials.name,
|
||||||
|
type: nodeCredentialType,
|
||||||
|
});
|
||||||
|
// if credential name-type combination is unique, take it
|
||||||
|
if (credsByName?.length === 1) {
|
||||||
|
// add found credential to cache
|
||||||
|
credentialsById[nodeCredentialType][credsByName[0].id] = {
|
||||||
|
id: credsByName[0].id.toString(),
|
||||||
|
name: credsByName[0].name,
|
||||||
|
};
|
||||||
|
node.credentials[nodeCredentialType] =
|
||||||
|
credentialsById[nodeCredentialType][credsByName[0].id];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing found - add invalid credentials to cache to prevent further DB checks
|
||||||
|
credentialsById[nodeCredentialType][nodeCredentials.id] = nodeCredentials;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get credentials from cache
|
||||||
|
node.credentials[nodeCredentialType] =
|
||||||
|
credentialsById[nodeCredentialType][nodeCredentials.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable no-await-in-loop */
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
|
|
@ -11,9 +11,40 @@ import {
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { getTimestampSyntax, resolveDataType } from '../utils';
|
|
||||||
|
|
||||||
import { ICredentialsDb } from '../..';
|
import config = require('../../../config');
|
||||||
|
import { DatabaseType, ICredentialsDb } from '../..';
|
||||||
|
|
||||||
|
function resolveDataType(dataType: string) {
|
||||||
|
const dbType = config.get('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
||||||
|
sqlite: {
|
||||||
|
json: 'simple-json',
|
||||||
|
},
|
||||||
|
postgresdb: {
|
||||||
|
datetime: 'timestamptz',
|
||||||
|
},
|
||||||
|
mysqldb: {},
|
||||||
|
mariadb: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return typeMap[dbType][dataType] ?? dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
function getTimestampSyntax() {
|
||||||
|
const dbType = config.get('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
const map: { [key in DatabaseType]: string } = {
|
||||||
|
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
||||||
|
postgresdb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
mysqldb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
mariadb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[dbType];
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class CredentialsEntity implements ICredentialsDb {
|
export class CredentialsEntity implements ICredentialsDb {
|
||||||
|
|
|
@ -2,9 +2,25 @@
|
||||||
import { WorkflowExecuteMode } from 'n8n-workflow';
|
import { WorkflowExecuteMode } from 'n8n-workflow';
|
||||||
|
|
||||||
import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
import { IExecutionFlattedDb, IWorkflowDb } from '../..';
|
import config = require('../../../config');
|
||||||
|
import { DatabaseType, IExecutionFlattedDb, IWorkflowDb } from '../..';
|
||||||
|
|
||||||
import { resolveDataType } from '../utils';
|
function resolveDataType(dataType: string) {
|
||||||
|
const dbType = config.get('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
||||||
|
sqlite: {
|
||||||
|
json: 'simple-json',
|
||||||
|
},
|
||||||
|
postgresdb: {
|
||||||
|
datetime: 'timestamptz',
|
||||||
|
},
|
||||||
|
mysqldb: {},
|
||||||
|
mariadb: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return typeMap[dbType][dataType] ?? dataType;
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class ExecutionEntity implements IExecutionFlattedDb {
|
export class ExecutionEntity implements IExecutionFlattedDb {
|
||||||
|
|
|
@ -12,9 +12,24 @@ import {
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
|
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
|
import config = require('../../../config');
|
||||||
|
import { DatabaseType } from '../../index';
|
||||||
import { ITagDb } from '../../Interfaces';
|
import { ITagDb } from '../../Interfaces';
|
||||||
import { WorkflowEntity } from './WorkflowEntity';
|
import { WorkflowEntity } from './WorkflowEntity';
|
||||||
import { getTimestampSyntax } from '../utils';
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
function getTimestampSyntax() {
|
||||||
|
const dbType = config.get('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
const map: { [key in DatabaseType]: string } = {
|
||||||
|
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
||||||
|
postgresdb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
mysqldb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
mariadb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[dbType];
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class TagEntity implements ITagDb {
|
export class TagEntity implements ITagDb {
|
||||||
|
|
|
@ -17,12 +17,41 @@ import {
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { IWorkflowDb } from '../..';
|
import config = require('../../../config');
|
||||||
|
import { DatabaseType, IWorkflowDb } from '../..';
|
||||||
import { getTimestampSyntax, resolveDataType } from '../utils';
|
|
||||||
|
|
||||||
import { TagEntity } from './TagEntity';
|
import { TagEntity } from './TagEntity';
|
||||||
|
|
||||||
|
function resolveDataType(dataType: string) {
|
||||||
|
const dbType = config.get('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
||||||
|
sqlite: {
|
||||||
|
json: 'simple-json',
|
||||||
|
},
|
||||||
|
postgresdb: {
|
||||||
|
datetime: 'timestamptz',
|
||||||
|
},
|
||||||
|
mysqldb: {},
|
||||||
|
mariadb: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return typeMap[dbType][dataType] ?? dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
function getTimestampSyntax() {
|
||||||
|
const dbType = config.get('database.type') as DatabaseType;
|
||||||
|
|
||||||
|
const map: { [key in DatabaseType]: string } = {
|
||||||
|
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
||||||
|
postgresdb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
mysqldb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
mariadb: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[dbType];
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class WorkflowEntity implements IWorkflowDb {
|
export class WorkflowEntity implements IWorkflowDb {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import config = require('../../../../config');
|
||||||
|
|
||||||
|
// replacing the credentials in workflows and execution
|
||||||
|
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||||
|
|
||||||
|
export class UpdateWorkflowCredentials1630451444017 implements MigrationInterface {
|
||||||
|
name = 'UpdateWorkflowCredentials1630451444017';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.get('database.tablePrefix');
|
||||||
|
|
||||||
|
const credentialsEntities = await queryRunner.query(`
|
||||||
|
SELECT id, name, type
|
||||||
|
FROM ${tablePrefix}credentials_entity
|
||||||
|
`);
|
||||||
|
|
||||||
|
const workflows = await queryRunner.query(`
|
||||||
|
SELECT id, nodes
|
||||||
|
FROM ${tablePrefix}workflow_entity
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
workflows.forEach(async (workflow) => {
|
||||||
|
const nodes = workflow.nodes;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}workflow_entity
|
||||||
|
SET nodes = :nodes
|
||||||
|
WHERE id = '${workflow.id}'
|
||||||
|
`,
|
||||||
|
{ nodes: JSON.stringify(nodes) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, workflowData
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE waitTill IS NOT NULL AND finished = 0
|
||||||
|
`);
|
||||||
|
|
||||||
|
const retryableExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, workflowData
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE waitTill IS NULL AND finished = 0 AND mode != 'retry'
|
||||||
|
ORDER BY startedAt DESC
|
||||||
|
LIMIT 200
|
||||||
|
`);
|
||||||
|
|
||||||
|
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||||
|
const data = execution.workflowData;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
data.nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}execution_entity
|
||||||
|
SET workflowData = :data
|
||||||
|
WHERE id = '${execution.id}'
|
||||||
|
`,
|
||||||
|
{ data: JSON.stringify(data) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.get('database.tablePrefix');
|
||||||
|
|
||||||
|
const credentialsEntities = await queryRunner.query(`
|
||||||
|
SELECT id, name, type
|
||||||
|
FROM ${tablePrefix}credentials_entity
|
||||||
|
`);
|
||||||
|
|
||||||
|
const workflows = await queryRunner.query(`
|
||||||
|
SELECT id, nodes
|
||||||
|
FROM ${tablePrefix}workflow_entity
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
workflows.forEach(async (workflow) => {
|
||||||
|
const nodes = workflow.nodes;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, creds] of allNodeCredentials) {
|
||||||
|
if (typeof creds === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||||
|
);
|
||||||
|
if (matchingCredentials) {
|
||||||
|
node.credentials[type] = matchingCredentials.name;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
node.credentials[type] = creds.name;
|
||||||
|
}
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}workflow_entity
|
||||||
|
SET nodes = :nodes
|
||||||
|
WHERE id = '${workflow.id}'
|
||||||
|
`,
|
||||||
|
{ nodes: JSON.stringify(nodes) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, workflowData
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE waitTill IS NOT NULL AND finished = 0
|
||||||
|
`);
|
||||||
|
|
||||||
|
const retryableExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, workflowData
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE waitTill IS NULL AND finished = 0 AND mode != 'retry'
|
||||||
|
ORDER BY startedAt DESC
|
||||||
|
LIMIT 200
|
||||||
|
`);
|
||||||
|
|
||||||
|
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||||
|
const data = execution.workflowData;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
data.nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, creds] of allNodeCredentials) {
|
||||||
|
if (typeof creds === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||||
|
);
|
||||||
|
if (matchingCredentials) {
|
||||||
|
node.credentials[type] = matchingCredentials.name;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
node.credentials[type] = creds.name;
|
||||||
|
}
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}execution_entity
|
||||||
|
SET workflowData = :data
|
||||||
|
WHERE id = '${execution.id}'
|
||||||
|
`,
|
||||||
|
{ data: JSON.stringify(data) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
|
||||||
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
|
||||||
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
|
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
|
||||||
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
|
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
|
||||||
|
import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWorkflowCredentials';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -22,4 +23,5 @@ export const mysqlMigrations = [
|
||||||
UniqueWorkflowNames1620826335440,
|
UniqueWorkflowNames1620826335440,
|
||||||
CertifyCorrectCollation1623936588000,
|
CertifyCorrectCollation1623936588000,
|
||||||
AddWaitColumnId1626183952959,
|
AddWaitColumnId1626183952959,
|
||||||
|
UpdateWorkflowCredentials1630451444017,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import config = require('../../../../config');
|
||||||
|
|
||||||
|
// replacing the credentials in workflows and execution
|
||||||
|
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||||
|
|
||||||
|
export class UpdateWorkflowCredentials1630419189837 implements MigrationInterface {
|
||||||
|
name = 'UpdateWorkflowCredentials1630419189837';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let tablePrefix = config.get('database.tablePrefix');
|
||||||
|
const schema = config.get('database.postgresdb.schema');
|
||||||
|
if (schema) {
|
||||||
|
tablePrefix = schema + '.' + tablePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialsEntities = await queryRunner.query(`
|
||||||
|
SELECT id, name, type
|
||||||
|
FROM ${tablePrefix}credentials_entity
|
||||||
|
`);
|
||||||
|
|
||||||
|
const workflows = await queryRunner.query(`
|
||||||
|
SELECT id, nodes
|
||||||
|
FROM ${tablePrefix}workflow_entity
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
workflows.forEach(async (workflow) => {
|
||||||
|
const nodes = workflow.nodes;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}workflow_entity
|
||||||
|
SET nodes = :nodes
|
||||||
|
WHERE id = '${workflow.id}'
|
||||||
|
`,
|
||||||
|
{ nodes: JSON.stringify(nodes) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||||
|
`);
|
||||||
|
|
||||||
|
const retryableExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE "waitTill" IS NULL AND finished = FALSE AND mode != 'retry'
|
||||||
|
ORDER BY "startedAt" DESC
|
||||||
|
LIMIT 200
|
||||||
|
`);
|
||||||
|
|
||||||
|
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||||
|
const data = execution.workflowData;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
data.nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}execution_entity
|
||||||
|
SET "workflowData" = :data
|
||||||
|
WHERE id = '${execution.id}'
|
||||||
|
`,
|
||||||
|
{ data: JSON.stringify(data) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let tablePrefix = config.get('database.tablePrefix');
|
||||||
|
const schema = config.get('database.postgresdb.schema');
|
||||||
|
if (schema) {
|
||||||
|
tablePrefix = schema + '.' + tablePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialsEntities = await queryRunner.query(`
|
||||||
|
SELECT id, name, type
|
||||||
|
FROM ${tablePrefix}credentials_entity
|
||||||
|
`);
|
||||||
|
|
||||||
|
const workflows = await queryRunner.query(`
|
||||||
|
SELECT id, nodes
|
||||||
|
FROM ${tablePrefix}workflow_entity
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
workflows.forEach(async (workflow) => {
|
||||||
|
const nodes = workflow.nodes;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, creds] of allNodeCredentials) {
|
||||||
|
if (typeof creds === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||||
|
);
|
||||||
|
if (matchingCredentials) {
|
||||||
|
node.credentials[type] = matchingCredentials.name;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
node.credentials[type] = creds.name;
|
||||||
|
}
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}workflow_entity
|
||||||
|
SET nodes = :nodes
|
||||||
|
WHERE id = '${workflow.id}'
|
||||||
|
`,
|
||||||
|
{ nodes: JSON.stringify(nodes) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||||
|
`);
|
||||||
|
|
||||||
|
const retryableExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM ${tablePrefix}execution_entity
|
||||||
|
WHERE "waitTill" IS NULL AND finished = FALSE AND mode != 'retry'
|
||||||
|
ORDER BY "startedAt" DESC
|
||||||
|
LIMIT 200
|
||||||
|
`);
|
||||||
|
|
||||||
|
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||||
|
const data = execution.workflowData;
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
data.nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, creds] of allNodeCredentials) {
|
||||||
|
if (typeof creds === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||||
|
);
|
||||||
|
if (matchingCredentials) {
|
||||||
|
node.credentials[type] = matchingCredentials.name;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
node.credentials[type] = creds.name;
|
||||||
|
}
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE ${tablePrefix}execution_entity
|
||||||
|
SET "workflowData" = :data
|
||||||
|
WHERE id = '${execution.id}'
|
||||||
|
`,
|
||||||
|
{ data: JSON.stringify(data) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedA
|
||||||
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
|
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
|
||||||
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
|
||||||
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
|
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
|
||||||
|
import { UpdateWorkflowCredentials1630419189837 } from './1630419189837-UpdateWorkflowCredentials';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -16,4 +17,5 @@ export const postgresMigrations = [
|
||||||
CreateTagEntity1617270242566,
|
CreateTagEntity1617270242566,
|
||||||
UniqueWorkflowNames1620824779533,
|
UniqueWorkflowNames1620824779533,
|
||||||
AddwaitTill1626176912946,
|
AddwaitTill1626176912946,
|
||||||
|
UpdateWorkflowCredentials1630419189837,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import config = require('../../../../config');
|
||||||
|
|
||||||
|
// replacing the credentials in workflows and execution
|
||||||
|
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||||
|
|
||||||
|
export class UpdateWorkflowCredentials1630330987096 implements MigrationInterface {
|
||||||
|
name = 'UpdateWorkflowCredentials1630330987096';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.get('database.tablePrefix');
|
||||||
|
|
||||||
|
const credentialsEntities = await queryRunner.query(`
|
||||||
|
SELECT id, name, type
|
||||||
|
FROM "${tablePrefix}credentials_entity"
|
||||||
|
`);
|
||||||
|
|
||||||
|
const workflows = await queryRunner.query(`
|
||||||
|
SELECT id, nodes
|
||||||
|
FROM "${tablePrefix}workflow_entity"
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
workflows.forEach(async (workflow) => {
|
||||||
|
const nodes = JSON.parse(workflow.nodes);
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE "${tablePrefix}workflow_entity"
|
||||||
|
SET nodes = :nodes
|
||||||
|
WHERE id = '${workflow.id}'
|
||||||
|
`,
|
||||||
|
{ nodes: JSON.stringify(nodes) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM "${tablePrefix}execution_entity"
|
||||||
|
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||||
|
`);
|
||||||
|
|
||||||
|
const retryableExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM "${tablePrefix}execution_entity"
|
||||||
|
WHERE "waitTill" IS NULL AND finished = 0 AND mode != 'retry'
|
||||||
|
ORDER BY "startedAt" DESC
|
||||||
|
LIMIT 200
|
||||||
|
`);
|
||||||
|
|
||||||
|
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||||
|
const data = JSON.parse(execution.workflowData);
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
data.nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, name] of allNodeCredentials) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.name === name && credentials.type === type,
|
||||||
|
);
|
||||||
|
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE "${tablePrefix}execution_entity"
|
||||||
|
SET "workflowData" = :data
|
||||||
|
WHERE id = '${execution.id}'
|
||||||
|
`,
|
||||||
|
{ data: JSON.stringify(data) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const tablePrefix = config.get('database.tablePrefix');
|
||||||
|
|
||||||
|
const credentialsEntities = await queryRunner.query(`
|
||||||
|
SELECT id, name, type
|
||||||
|
FROM "${tablePrefix}credentials_entity"
|
||||||
|
`);
|
||||||
|
|
||||||
|
const workflows = await queryRunner.query(`
|
||||||
|
SELECT id, nodes
|
||||||
|
FROM "${tablePrefix}workflow_entity"
|
||||||
|
`);
|
||||||
|
// @ts-ignore
|
||||||
|
workflows.forEach(async (workflow) => {
|
||||||
|
const nodes = JSON.parse(workflow.nodes);
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, creds] of allNodeCredentials) {
|
||||||
|
if (typeof creds === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||||
|
);
|
||||||
|
if (matchingCredentials) {
|
||||||
|
node.credentials[type] = matchingCredentials.name;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
node.credentials[type] = creds.name;
|
||||||
|
}
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE "${tablePrefix}workflow_entity"
|
||||||
|
SET nodes = :nodes
|
||||||
|
WHERE id = '${workflow.id}'
|
||||||
|
`,
|
||||||
|
{ nodes: JSON.stringify(nodes) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM "${tablePrefix}execution_entity"
|
||||||
|
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||||
|
`);
|
||||||
|
|
||||||
|
const retryableExecutions = await queryRunner.query(`
|
||||||
|
SELECT id, "workflowData"
|
||||||
|
FROM "${tablePrefix}execution_entity"
|
||||||
|
WHERE "waitTill" IS NULL AND finished = 0 AND mode != 'retry'
|
||||||
|
ORDER BY "startedAt" DESC
|
||||||
|
LIMIT 200
|
||||||
|
`);
|
||||||
|
|
||||||
|
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||||
|
const data = JSON.parse(execution.workflowData);
|
||||||
|
let credentialsUpdated = false;
|
||||||
|
// @ts-ignore
|
||||||
|
data.nodes.forEach((node) => {
|
||||||
|
if (node.credentials) {
|
||||||
|
const allNodeCredentials = Object.entries(node.credentials);
|
||||||
|
for (const [type, creds] of allNodeCredentials) {
|
||||||
|
if (typeof creds === 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
const matchingCredentials = credentialsEntities.find(
|
||||||
|
// @ts-ignore
|
||||||
|
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||||
|
);
|
||||||
|
if (matchingCredentials) {
|
||||||
|
node.credentials[type] = matchingCredentials.name;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
node.credentials[type] = creds.name;
|
||||||
|
}
|
||||||
|
credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (credentialsUpdated) {
|
||||||
|
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||||
|
`
|
||||||
|
UPDATE "${tablePrefix}execution_entity"
|
||||||
|
SET "workflowData" = :data
|
||||||
|
WHERE id = '${execution.id}'
|
||||||
|
`,
|
||||||
|
{ data: JSON.stringify(data) },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(updateQuery, updateParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedA
|
||||||
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
|
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
|
||||||
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
|
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
|
||||||
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
|
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
|
||||||
|
import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWorkflowCredentials';
|
||||||
|
|
||||||
export const sqliteMigrations = [
|
export const sqliteMigrations = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -16,4 +17,5 @@ export const sqliteMigrations = [
|
||||||
CreateTagEntity1617213344594,
|
CreateTagEntity1617213344594,
|
||||||
UniqueWorkflowNames1620821879465,
|
UniqueWorkflowNames1620821879465,
|
||||||
AddWaitColumn1621707690587,
|
AddWaitColumn1621707690587,
|
||||||
|
UpdateWorkflowCredentials1630330987096,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/* eslint-disable import/no-cycle */
|
|
||||||
import { DatabaseType } from '../index';
|
|
||||||
import { getConfigValueSync } from '../GenericHelpers';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the data type for the used database type
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {string} dataType
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export function resolveDataType(dataType: string) {
|
|
||||||
const dbType = getConfigValueSync('database.type') as DatabaseType;
|
|
||||||
|
|
||||||
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
|
||||||
sqlite: {
|
|
||||||
json: 'simple-json',
|
|
||||||
},
|
|
||||||
postgresdb: {
|
|
||||||
datetime: 'timestamptz',
|
|
||||||
},
|
|
||||||
mysqldb: {},
|
|
||||||
mariadb: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
return typeMap[dbType][dataType] ?? dataType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export function getTimestampSyntax() {
|
|
||||||
const dbType = getConfigValueSync('database.type') as DatabaseType;
|
|
||||||
|
|
||||||
const map: { [key in DatabaseType]: string } = {
|
|
||||||
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
|
||||||
postgresdb: 'CURRENT_TIMESTAMP(3)',
|
|
||||||
mysqldb: 'CURRENT_TIMESTAMP(3)',
|
|
||||||
mariadb: 'CURRENT_TIMESTAMP(3)',
|
|
||||||
};
|
|
||||||
|
|
||||||
return map[dbType];
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "0.87.0",
|
"version": "0.89.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",
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cron": "^1.7.1",
|
"@types/cron": "~1.7.1",
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.0.1",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/jest": "^26.0.13",
|
"@types/jest": "^26.0.13",
|
||||||
|
@ -44,13 +44,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"client-oauth2": "^4.2.5",
|
"client-oauth2": "^4.2.5",
|
||||||
"cron": "^1.7.2",
|
"cron": "~1.7.2",
|
||||||
"crypto-js": "~4.1.1",
|
"crypto-js": "~4.1.1",
|
||||||
"file-type": "^14.6.2",
|
"file-type": "^14.6.2",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"n8n-workflow": "~0.71.0",
|
"n8n-workflow": "~0.72.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"p-cancelable": "^2.0.0",
|
"p-cancelable": "^2.0.0",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
|
|
|
@ -98,6 +98,7 @@ export class Credentials extends ICredentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
data: this.data,
|
data: this.data,
|
||||||
|
|
|
@ -259,6 +259,10 @@ async function parseRequestObject(requestObject: IDataObject) {
|
||||||
axiosConfig.paramsSerializer = (params) => {
|
axiosConfig.paramsSerializer = (params) => {
|
||||||
return stringify(params, { arrayFormat: 'repeat' });
|
return stringify(params, { arrayFormat: 'repeat' });
|
||||||
};
|
};
|
||||||
|
} else if (requestObject.useQuerystring === false) {
|
||||||
|
axiosConfig.paramsSerializer = (params) => {
|
||||||
|
return stringify(params, { arrayFormat: 'indices' });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -301,7 +305,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (requestObject.json === false) {
|
if (requestObject.json === false || requestObject.json === undefined) {
|
||||||
// Prevent json parsing
|
// Prevent json parsing
|
||||||
axiosConfig.transformResponse = (res) => res;
|
axiosConfig.transformResponse = (res) => res;
|
||||||
}
|
}
|
||||||
|
@ -738,16 +742,20 @@ export async function requestOAuth2(
|
||||||
|
|
||||||
credentials.oauthTokenData = newToken.data;
|
credentials.oauthTokenData = newToken.data;
|
||||||
|
|
||||||
// Find the name of the credentials
|
// Find the credentials
|
||||||
if (!node.credentials || !node.credentials[credentialsType]) {
|
if (!node.credentials || !node.credentials[credentialsType]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The node "${node.name}" does not have credentials of type "${credentialsType}"!`,
|
`The node "${node.name}" does not have credentials of type "${credentialsType}"!`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const name = node.credentials[credentialsType];
|
const nodeCredentials = node.credentials[credentialsType];
|
||||||
|
|
||||||
// Save the refreshed token
|
// Save the refreshed token
|
||||||
await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials);
|
await additionalData.credentialsHelper.updateCredentials(
|
||||||
|
nodeCredentials,
|
||||||
|
credentialsType,
|
||||||
|
credentials,
|
||||||
|
);
|
||||||
|
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`,
|
`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`,
|
||||||
|
@ -955,25 +963,26 @@ export async function getCredentials(
|
||||||
} as ICredentialsExpressionResolveValues;
|
} as ICredentialsExpressionResolveValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = node.credentials[type];
|
const nodeCredentials = node.credentials[type];
|
||||||
|
|
||||||
if (name.charAt(0) === '=') {
|
// TODO: solve using credentials via expression
|
||||||
// If the credential name is an expression resolve it
|
// if (name.charAt(0) === '=') {
|
||||||
const additionalKeys = getAdditionalKeys(additionalData);
|
// // If the credential name is an expression resolve it
|
||||||
name = workflow.expression.getParameterValue(
|
// const additionalKeys = getAdditionalKeys(additionalData);
|
||||||
name,
|
// name = workflow.expression.getParameterValue(
|
||||||
runExecutionData || null,
|
// name,
|
||||||
runIndex || 0,
|
// runExecutionData || null,
|
||||||
itemIndex || 0,
|
// runIndex || 0,
|
||||||
node.name,
|
// itemIndex || 0,
|
||||||
connectionInputData || [],
|
// node.name,
|
||||||
mode,
|
// connectionInputData || [],
|
||||||
additionalKeys,
|
// mode,
|
||||||
) as string;
|
// additionalKeys,
|
||||||
}
|
// ) as string;
|
||||||
|
// }
|
||||||
|
|
||||||
const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted(
|
const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted(
|
||||||
name,
|
nodeCredentials,
|
||||||
type,
|
type,
|
||||||
mode,
|
mode,
|
||||||
false,
|
false,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Credentials } from '../src';
|
||||||
describe('Credentials', () => {
|
describe('Credentials', () => {
|
||||||
describe('without nodeType set', () => {
|
describe('without nodeType set', () => {
|
||||||
test('should be able to set and read key data without initial data set', () => {
|
test('should be able to set and read key data without initial data set', () => {
|
||||||
const credentials = new Credentials('testName', 'testType', []);
|
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []);
|
||||||
|
|
||||||
const key = 'key1';
|
const key = 'key1';
|
||||||
const password = 'password';
|
const password = 'password';
|
||||||
|
@ -23,7 +23,12 @@ describe('Credentials', () => {
|
||||||
const initialData = 4321;
|
const initialData = 4321;
|
||||||
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
|
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
|
||||||
|
|
||||||
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
|
const credentials = new Credentials(
|
||||||
|
{ id: null, name: 'testName' },
|
||||||
|
'testType',
|
||||||
|
[],
|
||||||
|
initialDataEncoded,
|
||||||
|
);
|
||||||
|
|
||||||
const newData = 1234;
|
const newData = 1234;
|
||||||
|
|
||||||
|
@ -46,7 +51,7 @@ describe('Credentials', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const credentials = new Credentials('testName', 'testType', nodeAccess);
|
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess);
|
||||||
|
|
||||||
const key = 'key1';
|
const key = 'key1';
|
||||||
const password = 'password';
|
const password = 'password';
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ICredentialsHelper,
|
ICredentialsHelper,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteWorkflowInfo,
|
IExecuteWorkflowInfo,
|
||||||
|
INodeCredentialsDetails,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeType,
|
INodeType,
|
||||||
|
@ -22,18 +23,21 @@ import {
|
||||||
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
|
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
|
||||||
|
|
||||||
export class CredentialsHelper extends ICredentialsHelper {
|
export class CredentialsHelper extends ICredentialsHelper {
|
||||||
getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> {
|
getDecrypted(
|
||||||
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
|
type: string,
|
||||||
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
return new Promise((res) => res({}));
|
return new Promise((res) => res({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentials(name: string, type: string): Promise<Credentials> {
|
getCredentials(nodeCredentials: INodeCredentialsDetails, type: string): Promise<Credentials> {
|
||||||
return new Promise((res) => {
|
return new Promise((res) => {
|
||||||
res(new Credentials('', '', [], ''));
|
res(new Credentials({ id: null, name: '' }, '', [], ''));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCredentials(
|
async updateCredentials(
|
||||||
name: string,
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
type: string,
|
type: string,
|
||||||
data: ICredentialDataDecryptedObject,
|
data: ICredentialDataDecryptedObject,
|
||||||
): Promise<void> {}
|
): Promise<void> {}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.110.0",
|
"version": "0.112.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",
|
||||||
|
@ -71,7 +71,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.71.0",
|
"n8n-workflow": "~0.72.0",
|
||||||
"sass": "^1.26.5",
|
"sass": "^1.26.5",
|
||||||
"normalize-wheel": "^1.0.1",
|
"normalize-wheel": "^1.0.1",
|
||||||
"prismjs": "^1.17.1",
|
"prismjs": "^1.17.1",
|
||||||
|
|
|
@ -244,7 +244,7 @@ export interface IActivationError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialsResponse extends ICredentialsEncrypted {
|
export interface ICredentialsResponse extends ICredentialsEncrypted {
|
||||||
id?: string;
|
id: string;
|
||||||
createdAt: number | string;
|
createdAt: number | string;
|
||||||
updatedAt: number | string;
|
updatedAt: number | string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -557,6 +557,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||||
);
|
);
|
||||||
|
|
||||||
const details: ICredentialsDecrypted = {
|
const details: ICredentialsDecrypted = {
|
||||||
|
id: this.credentialId,
|
||||||
name: this.credentialName,
|
name: this.credentialName,
|
||||||
type: this.credentialTypeName!,
|
type: this.credentialTypeName!,
|
||||||
data: data as unknown as ICredentialDataDecryptedObject,
|
data: data as unknown as ICredentialDataDecryptedObject,
|
||||||
|
@ -605,6 +606,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||||
);
|
);
|
||||||
|
|
||||||
const credentialDetails: ICredentialsDecrypted = {
|
const credentialDetails: ICredentialsDecrypted = {
|
||||||
|
id: this.credentialId,
|
||||||
name: this.credentialName,
|
name: this.credentialName,
|
||||||
type: this.credentialTypeName!,
|
type: this.credentialTypeName!,
|
||||||
data: data as unknown as ICredentialDataDecryptedObject,
|
data: data as unknown as ICredentialDataDecryptedObject,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div @keydown.stop :class="$style.container" v-if="credentialProperties.length">
|
<div @keydown.stop :class="$style.container" v-if="credentialProperties.length">
|
||||||
<div v-for="parameter in credentialProperties" :key="parameter.name">
|
<form v-for="parameter in credentialProperties" :key="parameter.name" autocomplete="off">
|
||||||
|
<!-- Why form? to break up inputs, to prevent Chrome autofill -->
|
||||||
<ParameterInputExpanded
|
<ParameterInputExpanded
|
||||||
:parameter="parameter"
|
:parameter="parameter"
|
||||||
:value="credentialData[parameter.name]"
|
:value="credentialData[parameter.name]"
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
:showValidationWarnings="showValidationWarnings"
|
:showValidationWarnings="showValidationWarnings"
|
||||||
@change="valueChanged"
|
@change="valueChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,15 @@
|
||||||
<el-col :span="10" class="parameter-name">
|
<el-col :span="10" class="parameter-name">
|
||||||
{{credentialTypeNames[credentialTypeDescription.name]}}:
|
{{credentialTypeNames[credentialTypeDescription.name]}}:
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
|
|
||||||
|
|
||||||
|
<el-col v-if="!isReadOnly" :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
|
||||||
<div :style="credentialInputWrapperStyle(credentialTypeDescription.name)">
|
<div :style="credentialInputWrapperStyle(credentialTypeDescription.name)">
|
||||||
<n8n-select :value="selected[credentialTypeDescription.name]" :disabled="isReadOnly" @change="(value) => credentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
|
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
|
||||||
<n8n-option
|
<n8n-option
|
||||||
v-for="(item) in credentialOptions[credentialTypeDescription.name]"
|
v-for="(item) in credentialOptions[credentialTypeDescription.name]"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
:value="item.name">
|
:value="item.id">
|
||||||
</n8n-option>
|
</n8n-option>
|
||||||
<n8n-option
|
<n8n-option
|
||||||
:key="NEW_CREDENTIALS_TEXT"
|
:key="NEW_CREDENTIALS_TEXT"
|
||||||
|
@ -34,10 +34,13 @@
|
||||||
<font-awesome-icon icon="exclamation-triangle" />
|
<font-awesome-icon icon="exclamation-triangle" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2" class="parameter-value credential-action">
|
<el-col v-if="!isReadOnly" :span="2" class="parameter-value credential-action">
|
||||||
<font-awesome-icon v-if="selected[credentialTypeDescription.name] && isCredentialValid(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
|
<font-awesome-icon v-if="isCredentialExisting(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col v-if="isReadOnly" :span="14" class="readonly-container" >
|
||||||
|
<n8n-input disabled :value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name" size="small" />
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
@ -49,12 +52,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
import {
|
import {
|
||||||
|
ICredentialsResponse,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
INodeUpdatePropertiesInformation,
|
INodeUpdatePropertiesInformation,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import {
|
import {
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
INodeCredentialDescription,
|
INodeCredentialDescription,
|
||||||
|
INodeCredentialsDetails,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -119,11 +124,17 @@ export default mixins(
|
||||||
}
|
}
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
selected(): {[type: string]: string} {
|
selected(): {[type: string]: INodeCredentialsDetails} {
|
||||||
return this.node.credentials || {};
|
return this.node.credentials || {};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getSelectedId(type: string) {
|
||||||
|
if (this.isCredentialExisting(type)) {
|
||||||
|
return this.selected[type].id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
credentialInputWrapperStyle (credentialType: string) {
|
credentialInputWrapperStyle (credentialType: string) {
|
||||||
let deductWidth = 0;
|
let deductWidth = 0;
|
||||||
const styles = {
|
const styles = {
|
||||||
|
@ -145,10 +156,10 @@ export default mixins(
|
||||||
|
|
||||||
this.newCredentialUnsubscribe = this.$store.subscribe((mutation, state) => {
|
this.newCredentialUnsubscribe = this.$store.subscribe((mutation, state) => {
|
||||||
if (mutation.type === 'credentials/upsertCredential' || mutation.type === 'credentials/enableOAuthCredential'){
|
if (mutation.type === 'credentials/upsertCredential' || mutation.type === 'credentials/enableOAuthCredential'){
|
||||||
this.credentialSelected(credentialType, mutation.payload.name);
|
this.onCredentialSelected(credentialType, mutation.payload.id);
|
||||||
}
|
}
|
||||||
if (mutation.type === 'credentials/deleteCredential') {
|
if (mutation.type === 'credentials/deleteCredential') {
|
||||||
this.credentialSelected(credentialType, mutation.payload.name);
|
this.clearSelectedCredential(credentialType);
|
||||||
this.stopListeningForNewCredentials();
|
this.stopListeningForNewCredentials();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -160,14 +171,52 @@ export default mixins(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
credentialSelected (credentialType: string, credentialName: string) {
|
clearSelectedCredential(credentialType: string) {
|
||||||
|
const node: INodeUi = this.node;
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
...(node.credentials || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
delete credentials[credentialType];
|
||||||
|
|
||||||
|
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||||
|
name: this.node.name,
|
||||||
|
properties: {
|
||||||
|
credentials,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$emit('credentialSelected', updateInformation);
|
||||||
|
},
|
||||||
|
|
||||||
|
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
|
||||||
let selected = undefined;
|
let selected = undefined;
|
||||||
if (credentialName === NEW_CREDENTIALS_TEXT) {
|
if (credentialId === NEW_CREDENTIALS_TEXT) {
|
||||||
this.listenForNewCredentials(credentialType);
|
this.listenForNewCredentials(credentialType);
|
||||||
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
selected = credentialName;
|
const selectedCredentials = this.$store.getters['credentials/getCredentialById'](credentialId);
|
||||||
|
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
|
||||||
|
|
||||||
|
selected = { id: selectedCredentials.id, name: selectedCredentials.name };
|
||||||
|
|
||||||
|
// if credentials has been string or neither id matched nor name matched uniquely
|
||||||
|
if (oldCredentials.id === null || (oldCredentials.id && !this.$store.getters['credentials/getCredentialByIdAndType'](oldCredentials.id, credentialType))) {
|
||||||
|
// update all nodes in the workflow with the same old/invalid credentials
|
||||||
|
this.$store.commit('replaceInvalidWorkflowCredentials', {
|
||||||
|
credentials: selected,
|
||||||
|
invalid: oldCredentials,
|
||||||
|
type: credentialType,
|
||||||
|
});
|
||||||
|
this.updateNodesCredentialsIssues();
|
||||||
|
this.$showMessage({
|
||||||
|
title: 'Node credentials updated',
|
||||||
|
message: `Nodes that used credentials "${oldCredentials.name}" have been updated to use "${selected.name}"`,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: INodeUi = this.node;
|
const node: INodeUi = this.node;
|
||||||
|
@ -209,18 +258,19 @@ export default mixins(
|
||||||
return node.issues.credentials[credentialTypeName];
|
return node.issues.credentials[credentialTypeName];
|
||||||
},
|
},
|
||||||
|
|
||||||
isCredentialValid(credentialType: string): boolean {
|
isCredentialExisting(credentialType: string): boolean {
|
||||||
const name = this.node.credentials[credentialType];
|
if (!this.node.credentials || !this.node.credentials[credentialType] || !this.node.credentials[credentialType].id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { id } = this.node.credentials[credentialType];
|
||||||
const options = this.credentialOptions[credentialType];
|
const options = this.credentialOptions[credentialType];
|
||||||
|
|
||||||
return options.find((option: ICredentialType) => option.name === name);
|
return !!options.find((option: ICredentialsResponse) => option.id === id);
|
||||||
},
|
},
|
||||||
|
|
||||||
editCredential(credentialType: string): void {
|
editCredential(credentialType: string): void {
|
||||||
const name = this.node.credentials[credentialType];
|
const { id } = this.node.credentials[credentialType];
|
||||||
const options = this.credentialOptions[credentialType];
|
this.$store.dispatch('ui/openExisitngCredential', { id });
|
||||||
const selected = options.find((option: ICredentialType) => option.name === name);
|
|
||||||
this.$store.dispatch('ui/openExisitngCredential', { id: selected.id });
|
|
||||||
|
|
||||||
this.listenForNewCredentials(credentialType);
|
this.listenForNewCredentials(credentialType);
|
||||||
},
|
},
|
||||||
|
@ -283,6 +333,10 @@ export default mixins(
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.readonly-container {
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,11 +7,12 @@ import {
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
INodeCredentialDescription,
|
INodeCredentialDescription,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
INodeParameters,
|
INodeCredentialsDetails,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodeIssues,
|
INodeIssues,
|
||||||
INodeIssueData,
|
INodeIssueData,
|
||||||
INodeIssueObjectProperty,
|
INodeIssueObjectProperty,
|
||||||
|
INodeParameters,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IRunData,
|
IRunData,
|
||||||
|
@ -196,7 +197,7 @@ export const nodeHelpers = mixins(
|
||||||
let userCredentials: ICredentialsResponse[] | null;
|
let userCredentials: ICredentialsResponse[] | null;
|
||||||
let credentialType: ICredentialType | null;
|
let credentialType: ICredentialType | null;
|
||||||
let credentialDisplayName: string;
|
let credentialDisplayName: string;
|
||||||
let selectedCredentials: string;
|
let selectedCredentials: INodeCredentialsDetails;
|
||||||
for (const credentialTypeDescription of nodeType!.credentials!) {
|
for (const credentialTypeDescription of nodeType!.credentials!) {
|
||||||
// Check if credentials should be displayed else ignore
|
// Check if credentials should be displayed else ignore
|
||||||
if (this.displayParameter(node.parameters, credentialTypeDescription, '') !== true) {
|
if (this.displayParameter(node.parameters, credentialTypeDescription, '') !== true) {
|
||||||
|
@ -218,15 +219,35 @@ export const nodeHelpers = mixins(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If they are set check if the value is valid
|
// If they are set check if the value is valid
|
||||||
selectedCredentials = node.credentials[credentialTypeDescription.name];
|
selectedCredentials = node.credentials[credentialTypeDescription.name] as INodeCredentialsDetails;
|
||||||
|
if (typeof selectedCredentials === 'string') {
|
||||||
|
selectedCredentials = {
|
||||||
|
id: null,
|
||||||
|
name: selectedCredentials,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
userCredentials = this.$store.getters['credentials/getCredentialsByType'](credentialTypeDescription.name);
|
userCredentials = this.$store.getters['credentials/getCredentialsByType'](credentialTypeDescription.name);
|
||||||
|
|
||||||
if (userCredentials === null) {
|
if (userCredentials === null) {
|
||||||
userCredentials = [];
|
userCredentials = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userCredentials.find((credentialData) => credentialData.name === selectedCredentials) === undefined) {
|
if (selectedCredentials.id) {
|
||||||
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials}" do not exist for "${credentialDisplayName}".`];
|
const idMatch = userCredentials.find((credentialData) => credentialData.id === selectedCredentials.id);
|
||||||
|
if (idMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameMatches = userCredentials.filter((credentialData) => credentialData.name === selectedCredentials.name);
|
||||||
|
if (nameMatches.length > 1) {
|
||||||
|
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials.name}" exist for "${credentialDisplayName}"`, "Credentials are not clearly identified. Please select the correct credentials."];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameMatches.length === 0) {
|
||||||
|
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials.name}" do not exist for "${credentialDisplayName}".`, "You can create credentials with the exact name and then they get auto-selected on refresh."];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,9 +90,15 @@ const module: Module<ICredentialsState, IRootState> = {
|
||||||
getCredentialById: (state: ICredentialsState) => {
|
getCredentialById: (state: ICredentialsState) => {
|
||||||
return (id: string) => state.credentials[id];
|
return (id: string) => state.credentials[id];
|
||||||
},
|
},
|
||||||
|
getCredentialByIdAndType: (state: ICredentialsState) => {
|
||||||
|
return (id: string, type: string) => {
|
||||||
|
const credential = state.credentials[id];
|
||||||
|
return !credential || credential.type !== type ? undefined : credential;
|
||||||
|
};
|
||||||
|
},
|
||||||
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
|
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
|
||||||
return (credentialType: string): ICredentialsResponse[] => {
|
return (credentialType: string): ICredentialsResponse[] => {
|
||||||
return getters.allCredentials.filter((credentialData: ICredentialsResponse) => credentialData.type === credentialType);
|
return getters.allCredentialsByType[credentialType];
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any
|
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any
|
||||||
|
|
|
@ -375,6 +375,32 @@ export const store = new Vuex.Store({
|
||||||
state.workflow.name = data.newName;
|
state.workflow.name = data.newName;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// replace invalid credentials in workflow
|
||||||
|
replaceInvalidWorkflowCredentials(state, {credentials, invalid, type }) {
|
||||||
|
state.workflow.nodes.forEach((node) => {
|
||||||
|
if (!node.credentials || !node.credentials[type]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodeCredentials = node.credentials[type];
|
||||||
|
|
||||||
|
if (typeof nodeCredentials === 'string' && nodeCredentials === invalid.name) {
|
||||||
|
node.credentials[type] = credentials;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeCredentials.id === null) {
|
||||||
|
if (nodeCredentials.name === invalid.name){
|
||||||
|
node.credentials[type] = credentials;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeCredentials.id === invalid.id) {
|
||||||
|
node.credentials[type] = credentials;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Nodes
|
// Nodes
|
||||||
addNode (state, nodeData: INodeUi) {
|
addNode (state, nodeData: INodeUi) {
|
||||||
if (!nodeData.hasOwnProperty('name')) {
|
if (!nodeData.hasOwnProperty('name')) {
|
||||||
|
|
|
@ -149,9 +149,11 @@ import {
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
Workflow,
|
Workflow,
|
||||||
IRun,
|
IRun,
|
||||||
|
INodeCredentialsDetails,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
IConnectionsUi,
|
IConnectionsUi,
|
||||||
|
ICredentialsResponse,
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
|
@ -327,6 +329,7 @@ export default mixins(
|
||||||
ctrlKeyPressed: false,
|
ctrlKeyPressed: false,
|
||||||
stopExecutionInProgress: false,
|
stopExecutionInProgress: false,
|
||||||
blankRedirect: false,
|
blankRedirect: false,
|
||||||
|
credentialsUpdated: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
|
@ -495,8 +498,10 @@ export default mixins(
|
||||||
this.$store.commit('setWorkflowTagIds', tagIds || []);
|
this.$store.commit('setWorkflowTagIds', tagIds || []);
|
||||||
|
|
||||||
await this.addNodes(data.nodes, data.connections);
|
await this.addNodes(data.nodes, data.connections);
|
||||||
|
if (!this.credentialsUpdated) {
|
||||||
|
this.$store.commit('setStateDirty', false);
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', false);
|
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
|
|
||||||
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
||||||
|
@ -1865,6 +1870,47 @@ export default mixins(
|
||||||
this.deselectAllNodes();
|
this.deselectAllNodes();
|
||||||
this.nodeSelectedByName(newName);
|
this.nodeSelectedByName(newName);
|
||||||
},
|
},
|
||||||
|
matchCredentials(node: INodeUi) {
|
||||||
|
if (!node.credentials) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.entries(node.credentials).forEach(([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
|
||||||
|
const credentialOptions = this.$store.getters['credentials/getCredentialsByType'](nodeCredentialType) as ICredentialsResponse[];
|
||||||
|
|
||||||
|
// Check if workflows applies old credentials style
|
||||||
|
if (typeof nodeCredentials === 'string') {
|
||||||
|
nodeCredentials = {
|
||||||
|
id: null,
|
||||||
|
name: nodeCredentials,
|
||||||
|
};
|
||||||
|
this.credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeCredentials.id) {
|
||||||
|
// Check whether the id is matching with a credential
|
||||||
|
const credentialsForId = credentialOptions.find((optionData: ICredentialsResponse) => optionData.id === nodeCredentials.id);
|
||||||
|
if (credentialsForId) {
|
||||||
|
if (credentialsForId.name !== nodeCredentials.name) {
|
||||||
|
node.credentials![nodeCredentialType].name = credentialsForId.name;
|
||||||
|
this.credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match for id found or old credentials type used
|
||||||
|
node.credentials![nodeCredentialType] = nodeCredentials;
|
||||||
|
|
||||||
|
// check if only one option with the name would exist
|
||||||
|
const credentialsForName = credentialOptions.filter((optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name);
|
||||||
|
|
||||||
|
// only one option exists for the name, take it
|
||||||
|
if (credentialsForName.length === 1) {
|
||||||
|
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
|
||||||
|
this.credentialsUpdated = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
async addNodes (nodes: INodeUi[], connections?: IConnections) {
|
async addNodes (nodes: INodeUi[], connections?: IConnections) {
|
||||||
if (!nodes || !nodes.length) {
|
if (!nodes || !nodes.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -1914,6 +1960,9 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check and match credentials, apply new format if old is used
|
||||||
|
this.matchCredentials(node);
|
||||||
|
|
||||||
foundNodeIssues = this.getNodeIssues(nodeType, node);
|
foundNodeIssues = this.getNodeIssues(nodeType, node);
|
||||||
|
|
||||||
if (foundNodeIssues !== null) {
|
if (foundNodeIssues !== null) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-node-dev",
|
"name": "n8n-node-dev",
|
||||||
"version": "0.27.0",
|
"version": "0.29.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",
|
||||||
|
@ -60,8 +60,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.87.0",
|
"n8n-core": "~0.89.0",
|
||||||
"n8n-workflow": "~0.71.0",
|
"n8n-workflow": "~0.72.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",
|
||||||
|
|
|
@ -312,7 +312,7 @@ export class AwsDynamoDB implements INodeType {
|
||||||
if (scan === true) {
|
if (scan === true) {
|
||||||
body['FilterExpression'] = this.getNodeParameter('filterExpression', i) as string;
|
body['FilterExpression'] = this.getNodeParameter('filterExpression', i) as string;
|
||||||
} else {
|
} else {
|
||||||
body['KeyConditionExpression'] = this.getNodeParameter('KeyConditionExpression', i) as string;
|
body['KeyConditionExpression'] = this.getNodeParameter('keyConditionExpression', i) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -18,5 +18,8 @@
|
||||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.ftp/"
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.ftp/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"alias": [
|
||||||
|
"SFTP"
|
||||||
|
]
|
||||||
}
|
}
|
|
@ -50,7 +50,7 @@ export async function nextCloudApiRequest(this: IHookFunctions | IExecuteFunctio
|
||||||
|
|
||||||
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
|
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
|
||||||
|
|
||||||
if (resource === 'user') {
|
if (resource === 'user' || operation === 'share') {
|
||||||
options.uri = options.uri.replace('/remote.php/webdav', '');
|
options.uri = options.uri.replace('/remote.php/webdav', '');
|
||||||
}
|
}
|
||||||
return await this.helpers.request(options);
|
return await this.helpers.request(options);
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseString,
|
parseString,
|
||||||
} from 'xml2js';
|
} from 'xml2js';
|
||||||
|
@ -135,6 +137,11 @@ export class NextCloud implements INodeType {
|
||||||
value: 'move',
|
value: 'move',
|
||||||
description: 'Move a file',
|
description: 'Move a file',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Share',
|
||||||
|
value: 'share',
|
||||||
|
description: 'Share a file',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Upload',
|
name: 'Upload',
|
||||||
value: 'upload',
|
value: 'upload',
|
||||||
|
@ -182,6 +189,11 @@ export class NextCloud implements INodeType {
|
||||||
value: 'move',
|
value: 'move',
|
||||||
description: 'Move a folder',
|
description: 'Move a folder',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Share',
|
||||||
|
value: 'share',
|
||||||
|
description: 'Share a folder',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
default: 'create',
|
default: 'create',
|
||||||
description: 'The operation to perform.',
|
description: 'The operation to perform.',
|
||||||
|
@ -472,7 +484,223 @@ export class NextCloud implements INodeType {
|
||||||
description: 'Name of the binary property which contains<br />the data for the file to be uploaded.',
|
description: 'Name of the binary property which contains<br />the data for the file to be uploaded.',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// file:share
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'File Path',
|
||||||
|
name: 'path',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
placeholder: '/invoices/2019/invoice_1.pdf',
|
||||||
|
description: 'The file path of the file to share. Has to contain the full path. The path should start with "/"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Share Type',
|
||||||
|
name: 'shareType',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Circle',
|
||||||
|
value: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Email',
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Group',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Public Link',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 0,
|
||||||
|
description: 'The share permissions to set',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Circle ID',
|
||||||
|
name: 'circleId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
shareType: [
|
||||||
|
7,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The ID of the circle to share with',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Email',
|
||||||
|
name: 'email',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
shareType: [
|
||||||
|
4,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The Email address to share with',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Group ID',
|
||||||
|
name: 'groupId',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
shareType: [
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The ID of the group to share with',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User',
|
||||||
|
name: 'user',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
shareType: [
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The user to share with',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Password',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/resource': [
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
],
|
||||||
|
'/operation': [
|
||||||
|
'share',
|
||||||
|
],
|
||||||
|
'/shareType': [
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Optional search string.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Permissions',
|
||||||
|
name: 'permissions',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'All',
|
||||||
|
value: 31,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Read',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 1,
|
||||||
|
description: 'The share permissions to set',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// folder
|
// folder
|
||||||
|
@ -894,6 +1122,35 @@ export class NextCloud implements INodeType {
|
||||||
const toPath = this.getNodeParameter('toPath', i) as string;
|
const toPath = this.getNodeParameter('toPath', i) as string;
|
||||||
headers.Destination = `${credentials.webDavUrl}/${encodeURI(toPath)}`;
|
headers.Destination = `${credentials.webDavUrl}/${encodeURI(toPath)}`;
|
||||||
|
|
||||||
|
} else if (operation === 'share') {
|
||||||
|
// ----------------------------------
|
||||||
|
// share
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
requestMethod = 'POST';
|
||||||
|
|
||||||
|
endpoint = 'ocs/v2.php/apps/files_sharing/api/v1/shares';
|
||||||
|
|
||||||
|
headers['OCS-APIRequest'] = true;
|
||||||
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
|
||||||
|
const bodyParameters = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
|
||||||
|
bodyParameters.path = this.getNodeParameter('path', i) as string;
|
||||||
|
bodyParameters.shareType = this.getNodeParameter('shareType', i) as number;
|
||||||
|
|
||||||
|
if (bodyParameters.shareType === 0) {
|
||||||
|
bodyParameters.shareWith = this.getNodeParameter('user', i) as string;
|
||||||
|
} else if (bodyParameters.shareType === 7) {
|
||||||
|
bodyParameters.shareWith = this.getNodeParameter('circleId', i) as number;
|
||||||
|
} else if (bodyParameters.shareType === 4) {
|
||||||
|
bodyParameters.shareWith = this.getNodeParameter('email', i) as string;
|
||||||
|
} else if (bodyParameters.shareType === 1) {
|
||||||
|
bodyParameters.shareWith = this.getNodeParameter('groupId', i) as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
body = new URLSearchParams(bodyParameters).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (resource === 'user') {
|
} else if (resource === 'user') {
|
||||||
|
@ -1032,6 +1289,23 @@ export class NextCloud implements INodeType {
|
||||||
|
|
||||||
items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData, endpoint);
|
items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData, endpoint);
|
||||||
|
|
||||||
|
} else if (['file', 'folder'].includes(resource) && operation === 'share') {
|
||||||
|
const jsonResponseData: IDataObject = await new Promise((resolve, reject) => {
|
||||||
|
parseString(responseData, { explicitArray: false }, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.ocs.meta.status !== 'ok') {
|
||||||
|
return reject(new Error(data.ocs.meta.message || data.ocs.meta.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data.ocs.data as IDataObject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
returnData.push(jsonResponseData as IDataObject);
|
||||||
|
|
||||||
} else if (resource === 'user') {
|
} else if (resource === 'user') {
|
||||||
|
|
||||||
if (operation !== 'getAll') {
|
if (operation !== 'getAll') {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
"nodeVersion": "1.0",
|
"nodeVersion": "1.0",
|
||||||
"codexVersion": "1.0",
|
"codexVersion": "1.0",
|
||||||
"categories": [
|
"categories": [
|
||||||
"Core Nodes"
|
"Core Nodes",
|
||||||
|
"Utility"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"primaryDocumentation": [
|
"primaryDocumentation": [
|
||||||
|
@ -13,13 +14,13 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"alias": [
|
"alias": [
|
||||||
"error",
|
"Throw error",
|
||||||
"throw",
|
"Error",
|
||||||
"exception"
|
"Exception"
|
||||||
],
|
],
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": [
|
"Core Nodes":[
|
||||||
"Flow"
|
"Helpers"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -692,6 +692,7 @@ export class Wait implements INodeType {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const mimeType = headers['content-type'] || 'application/json';
|
const mimeType = headers['content-type'] || 'application/json';
|
||||||
if (mimeType.includes('multipart/form-data')) {
|
if (mimeType.includes('multipart/form-data')) {
|
||||||
|
// @ts-ignore
|
||||||
const form = new formidable.IncomingForm({ multiples: true });
|
const form = new formidable.IncomingForm({ multiples: true });
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -407,6 +407,7 @@ export class Webhook implements INodeType {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const mimeType = headers['content-type'] || 'application/json';
|
const mimeType = headers['content-type'] || 'application/json';
|
||||||
if (mimeType.includes('multipart/form-data')) {
|
if (mimeType.includes('multipart/form-data')) {
|
||||||
|
// @ts-ignore
|
||||||
const form = new formidable.IncomingForm({ multiples: true });
|
const form = new formidable.IncomingForm({ multiples: true });
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -42,7 +42,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64');
|
const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64');
|
||||||
options.uri = `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`;
|
options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`;
|
||||||
options.headers!['Authorization'] = `Basic ${base64Key}`;
|
options.headers!['Authorization'] = `Basic ${base64Key}`;
|
||||||
return await this.helpers.request!(options);
|
return await this.helpers.request!(options);
|
||||||
} else {
|
} else {
|
||||||
|
@ -52,7 +52,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
|
||||||
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
||||||
}
|
}
|
||||||
|
|
||||||
options.uri = `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`;
|
options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`;
|
||||||
|
|
||||||
return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options);
|
return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export const ticketOperations = [
|
export const ticketOperations = [
|
||||||
{
|
{
|
||||||
|
@ -35,6 +35,11 @@ export const ticketOperations = [
|
||||||
value: 'getAll',
|
value: 'getAll',
|
||||||
description: 'Get all tickets',
|
description: 'Get all tickets',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Recover',
|
||||||
|
value: 'recover',
|
||||||
|
description: 'Recover a suspended ticket',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Update',
|
name: 'Update',
|
||||||
value: 'update',
|
value: 'update',
|
||||||
|
@ -48,9 +53,9 @@ export const ticketOperations = [
|
||||||
|
|
||||||
export const ticketFields = [
|
export const ticketFields = [
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* ticket:create */
|
/* ticket:create */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
{
|
{
|
||||||
displayName: 'Description',
|
displayName: 'Description',
|
||||||
name: 'description',
|
name: 'description',
|
||||||
|
@ -265,9 +270,9 @@ export const ticketFields = [
|
||||||
description: `Object of values to set as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets">here</a>.`,
|
description: `Object of values to set as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets">here</a>.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* ticket:update */
|
/* ticket:update */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
{
|
{
|
||||||
displayName: 'Ticket ID',
|
displayName: 'Ticket ID',
|
||||||
name: 'id',
|
name: 'id',
|
||||||
|
@ -323,6 +328,13 @@ export const ticketFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Assignee Email',
|
||||||
|
name: 'assigneeEmail',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The e-mail address of the assignee',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Custom Fields',
|
displayName: 'Custom Fields',
|
||||||
name: 'customFieldsUi',
|
name: 'customFieldsUi',
|
||||||
|
@ -375,6 +387,20 @@ export const ticketFields = [
|
||||||
default: '',
|
default: '',
|
||||||
description: 'The group this ticket is assigned to',
|
description: 'The group this ticket is assigned to',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Internal Note',
|
||||||
|
name: 'internalNote',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Internal Ticket Note (Accepts HTML)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Public Reply',
|
||||||
|
name: 'publicReply',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Public ticket reply',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Recipient',
|
displayName: 'Recipient',
|
||||||
name: 'recipient',
|
name: 'recipient',
|
||||||
|
@ -478,10 +504,38 @@ export const ticketFields = [
|
||||||
},
|
},
|
||||||
description: `Object of values to update as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets">here</a>.`,
|
description: `Object of values to update as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets">here</a>.`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
/* -------------------------------------------------------------------------- */
|
displayName: 'Ticket Type',
|
||||||
/* ticket:get */
|
name: 'ticketType',
|
||||||
/* -------------------------------------------------------------------------- */
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Regular',
|
||||||
|
value: 'regular',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Suspended',
|
||||||
|
value: 'suspended',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'regular',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'ticket',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
'delete',
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* ticket:get */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
{
|
{
|
||||||
displayName: 'Ticket ID',
|
displayName: 'Ticket ID',
|
||||||
name: 'id',
|
name: 'id',
|
||||||
|
@ -496,13 +550,37 @@ export const ticketFields = [
|
||||||
operation: [
|
operation: [
|
||||||
'get',
|
'get',
|
||||||
],
|
],
|
||||||
|
ticketType: [
|
||||||
|
'regular',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: 'Ticket ID',
|
description: 'Ticket ID',
|
||||||
},
|
},
|
||||||
/* -------------------------------------------------------------------------- */
|
{
|
||||||
/* ticket:getAll */
|
displayName: 'Suspended Ticket ID',
|
||||||
/* -------------------------------------------------------------------------- */
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'ticket',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
ticketType: [
|
||||||
|
'suspended',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Ticket ID',
|
||||||
|
},
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* ticket:getAll */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
{
|
{
|
||||||
displayName: 'Return All',
|
displayName: 'Return All',
|
||||||
name: 'returnAll',
|
name: 'returnAll',
|
||||||
|
@ -561,6 +639,37 @@ export const ticketFields = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Group',
|
||||||
|
name: 'group',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getGroups',
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/ticketType': [
|
||||||
|
'regular',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The group to search',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Query',
|
||||||
|
name: 'query',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/ticketType': [
|
||||||
|
'regular',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: '<a href="https://developer.zendesk.com/api-reference/ticketing/ticket-management/search/#syntax-examples">Query syntax</a> to search tickets',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Sort By',
|
displayName: 'Sort By',
|
||||||
name: 'sortBy',
|
name: 'sortBy',
|
||||||
|
@ -596,21 +705,28 @@ export const ticketFields = [
|
||||||
type: 'options',
|
type: 'options',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'Asc',
|
name: 'Ascending',
|
||||||
value: 'asc',
|
value: 'asc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Desc',
|
name: 'Descending',
|
||||||
value: 'desc',
|
value: 'desc',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'desc',
|
default: 'asc',
|
||||||
description: 'Sort order',
|
description: 'Sort order',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Status',
|
displayName: 'Status',
|
||||||
name: 'status',
|
name: 'status',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/ticketType': [
|
||||||
|
'regular',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'Open',
|
name: 'Open',
|
||||||
|
@ -639,9 +755,9 @@ export const ticketFields = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* ticket:delete */
|
/* ticket:delete */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
{
|
{
|
||||||
displayName: 'Ticket ID',
|
displayName: 'Ticket ID',
|
||||||
name: 'id',
|
name: 'id',
|
||||||
|
@ -656,8 +772,52 @@ export const ticketFields = [
|
||||||
operation: [
|
operation: [
|
||||||
'delete',
|
'delete',
|
||||||
],
|
],
|
||||||
|
ticketType: [
|
||||||
|
'regular',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: 'Ticket ID',
|
description: 'Ticket ID',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Suspended Ticket ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'ticket',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'delete',
|
||||||
|
],
|
||||||
|
ticketType: [
|
||||||
|
'suspended',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Ticket ID',
|
||||||
|
},
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* ticket:recover */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
{
|
||||||
|
displayName: 'Suspended Ticket ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'ticket',
|
||||||
|
],
|
||||||
|
operation: [
|
||||||
|
'recover',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
] as INodeProperties[];
|
] as INodeProperties[];
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
|
|
||||||
export interface IComment {
|
export interface IComment {
|
||||||
body?: string;
|
body?: string;
|
||||||
|
html_body?: string;
|
||||||
|
public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITicket {
|
export interface ITicket {
|
||||||
|
@ -16,4 +18,5 @@ export interface ITicket {
|
||||||
status?: string;
|
status?: string;
|
||||||
recipient?: string;
|
recipient?: string;
|
||||||
custom_fields?: IDataObject[];
|
custom_fields?: IDataObject[];
|
||||||
|
assignee_email?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ import {
|
||||||
import {
|
import {
|
||||||
IComment,
|
IComment,
|
||||||
ITicket,
|
ITicket,
|
||||||
} from './TicketInterface';
|
} from './TicketInterface';
|
||||||
|
|
||||||
export class Zendesk implements INodeType {
|
export class Zendesk implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
@ -99,7 +99,7 @@ export class Zendesk implements INodeType {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'apiToken',
|
default: 'apiToken',
|
||||||
description: 'The resource to operate on.',
|
description: 'The resource to operate on',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Resource',
|
displayName: 'Resource',
|
||||||
|
@ -109,7 +109,7 @@ export class Zendesk implements INodeType {
|
||||||
{
|
{
|
||||||
name: 'Ticket',
|
name: 'Ticket',
|
||||||
value: 'ticket',
|
value: 'ticket',
|
||||||
description: 'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support.',
|
description: 'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ticket Field',
|
name: 'Ticket Field',
|
||||||
|
@ -128,7 +128,7 @@ export class Zendesk implements INodeType {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'ticket',
|
default: 'ticket',
|
||||||
description: 'Resource to consume.',
|
description: 'Resource to consume',
|
||||||
},
|
},
|
||||||
// TICKET
|
// TICKET
|
||||||
...ticketOperations,
|
...ticketOperations,
|
||||||
|
@ -286,12 +286,12 @@ export class Zendesk implements INodeType {
|
||||||
body: description,
|
body: description,
|
||||||
};
|
};
|
||||||
const body: ITicket = {
|
const body: ITicket = {
|
||||||
comment,
|
comment,
|
||||||
};
|
};
|
||||||
if (jsonParameters) {
|
if (jsonParameters) {
|
||||||
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
|
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
|
||||||
|
|
||||||
if (additionalFieldsJson !== '' ) {
|
if (additionalFieldsJson !== '') {
|
||||||
|
|
||||||
if (validateJSON(additionalFieldsJson) !== undefined) {
|
if (validateJSON(additionalFieldsJson) !== undefined) {
|
||||||
|
|
||||||
|
@ -343,7 +343,7 @@ export class Zendesk implements INodeType {
|
||||||
if (jsonParameters) {
|
if (jsonParameters) {
|
||||||
const updateFieldsJson = this.getNodeParameter('updateFieldsJson', i) as string;
|
const updateFieldsJson = this.getNodeParameter('updateFieldsJson', i) as string;
|
||||||
|
|
||||||
if (updateFieldsJson !== '' ) {
|
if (updateFieldsJson !== '') {
|
||||||
|
|
||||||
if (validateJSON(updateFieldsJson) !== undefined) {
|
if (validateJSON(updateFieldsJson) !== undefined) {
|
||||||
|
|
||||||
|
@ -382,44 +382,87 @@ export class Zendesk implements INodeType {
|
||||||
if (updateFields.customFieldsUi) {
|
if (updateFields.customFieldsUi) {
|
||||||
body.custom_fields = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
|
body.custom_fields = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
|
||||||
}
|
}
|
||||||
|
if (updateFields.assigneeEmail) {
|
||||||
|
body.assignee_email = updateFields.assigneeEmail as string;
|
||||||
|
}
|
||||||
|
if (updateFields.internalNote) {
|
||||||
|
const comment: IComment = {
|
||||||
|
html_body: updateFields.internalNote as string,
|
||||||
|
public: false,
|
||||||
|
};
|
||||||
|
body.comment = comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateFields.publicReply) {
|
||||||
|
const comment: IComment = {
|
||||||
|
body: updateFields.publicReply as string,
|
||||||
|
public: true,
|
||||||
|
};
|
||||||
|
body.comment = comment;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body });
|
responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body });
|
||||||
responseData = responseData.ticket;
|
responseData = responseData.ticket;
|
||||||
}
|
}
|
||||||
//https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket
|
//https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket
|
||||||
|
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#show-suspended-ticket
|
||||||
if (operation === 'get') {
|
if (operation === 'get') {
|
||||||
|
const ticketType = this.getNodeParameter('ticketType', i) as string;
|
||||||
const ticketId = this.getNodeParameter('id', i) as string;
|
const ticketId = this.getNodeParameter('id', i) as string;
|
||||||
responseData = await zendeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {});
|
const endpoint = (ticketType === 'regular') ? `/tickets/${ticketId}` : `/suspended_tickets/${ticketId}`;
|
||||||
responseData = responseData.ticket;
|
responseData = await zendeskApiRequest.call(this, 'GET', endpoint, {});
|
||||||
|
responseData = responseData.ticket || responseData.suspended_ticket;
|
||||||
}
|
}
|
||||||
//https://developer.zendesk.com/rest_api/docs/support/search#list-search-results
|
//https://developer.zendesk.com/rest_api/docs/support/search#list-search-results
|
||||||
|
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#list-suspended-tickets
|
||||||
if (operation === 'getAll') {
|
if (operation === 'getAll') {
|
||||||
|
const ticketType = this.getNodeParameter('ticketType', i) as string;
|
||||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
qs.query = 'type:ticket';
|
qs.query = 'type:ticket';
|
||||||
|
if (options.query) {
|
||||||
|
qs.query += ` ${options.query}`;
|
||||||
|
}
|
||||||
if (options.status) {
|
if (options.status) {
|
||||||
qs.query += ` status:${options.status}`;
|
qs.query += ` status:${options.status}`;
|
||||||
}
|
}
|
||||||
|
if (options.group) {
|
||||||
|
qs.query += ` group:${options.group}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.sortBy) {
|
if (options.sortBy) {
|
||||||
qs.sort_by = options.sortBy;
|
qs.sort_by = options.sortBy;
|
||||||
}
|
}
|
||||||
if (options.sortOrder) {
|
if (options.sortOrder) {
|
||||||
qs.sort_order = options.sortOrder;
|
qs.sort_order = options.sortOrder;
|
||||||
}
|
}
|
||||||
|
const endpoint = (ticketType === 'regular') ? `/search` : `/suspended_tickets`;
|
||||||
|
const property = (ticketType === 'regular') ? 'results' : 'suspended_tickets';
|
||||||
if (returnAll) {
|
if (returnAll) {
|
||||||
responseData = await zendeskApiRequestAllItems.call(this, 'results', 'GET', `/search`, {}, qs);
|
responseData = await zendeskApiRequestAllItems.call(this, property, 'GET', endpoint, {}, qs);
|
||||||
} else {
|
} else {
|
||||||
const limit = this.getNodeParameter('limit', i) as number;
|
const limit = this.getNodeParameter('limit', i) as number;
|
||||||
qs.per_page = limit;
|
qs.per_page = limit;
|
||||||
responseData = await zendeskApiRequest.call(this, 'GET', `/search`, {}, qs);
|
responseData = await zendeskApiRequest.call(this, 'GET', endpoint, {}, qs);
|
||||||
responseData = responseData.results;
|
responseData = responseData.results || responseData.suspended_tickets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket
|
//https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket
|
||||||
|
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#delete-suspended-ticket
|
||||||
if (operation === 'delete') {
|
if (operation === 'delete') {
|
||||||
|
const ticketType = this.getNodeParameter('ticketType', i) as string;
|
||||||
|
const ticketId = this.getNodeParameter('id', i) as string;
|
||||||
|
const endpoint = (ticketType === 'regular') ? `/tickets/${ticketId}` : `/suspended_tickets/${ticketId}`;
|
||||||
|
responseData = await zendeskApiRequest.call(this, 'DELETE', endpoint, {});
|
||||||
|
responseData = { success: true };
|
||||||
|
}
|
||||||
|
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#recover-suspended-ticket
|
||||||
|
if (operation === 'recover') {
|
||||||
const ticketId = this.getNodeParameter('id', i) as string;
|
const ticketId = this.getNodeParameter('id', i) as string;
|
||||||
try {
|
try {
|
||||||
responseData = await zendeskApiRequest.call(this, 'DELETE', `/tickets/${ticketId}`, {});
|
responseData = await zendeskApiRequest.call(this, 'PUT', `/suspended_tickets/${ticketId}/recover`, {});
|
||||||
|
responseData = responseData.ticket;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeApiError(this.getNode(), error);
|
throw new NodeApiError(this.getNode(), error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-nodes-base",
|
"name": "n8n-nodes-base",
|
||||||
"version": "0.139.0",
|
"version": "0.141.0",
|
||||||
"description": "Base nodes of n8n",
|
"description": "Base nodes of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -638,7 +638,7 @@
|
||||||
"@types/aws4": "^1.5.1",
|
"@types/aws4": "^1.5.1",
|
||||||
"@types/basic-auth": "^1.1.2",
|
"@types/basic-auth": "^1.1.2",
|
||||||
"@types/cheerio": "^0.22.15",
|
"@types/cheerio": "^0.22.15",
|
||||||
"@types/cron": "^1.7.1",
|
"@types/cron": "~1.7.1",
|
||||||
"@types/eventsource": "^1.1.2",
|
"@types/eventsource": "^1.1.2",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/formidable": "^1.0.31",
|
"@types/formidable": "^1.0.31",
|
||||||
|
@ -663,7 +663,7 @@
|
||||||
"@types/xml2js": "^0.4.3",
|
"@types/xml2js": "^0.4.3",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
"jest": "^26.4.2",
|
"jest": "^26.4.2",
|
||||||
"n8n-workflow": "~0.71.0",
|
"n8n-workflow": "~0.72.0",
|
||||||
"nodelinter": "^0.1.9",
|
"nodelinter": "^0.1.9",
|
||||||
"ts-jest": "^26.3.0",
|
"ts-jest": "^26.3.0",
|
||||||
"tslint": "^6.1.2",
|
"tslint": "^6.1.2",
|
||||||
|
@ -678,7 +678,7 @@
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
"change-case": "^4.1.1",
|
"change-case": "^4.1.1",
|
||||||
"cheerio": "1.0.0-rc.6",
|
"cheerio": "1.0.0-rc.6",
|
||||||
"cron": "^1.7.2",
|
"cron": "~1.7.2",
|
||||||
"eventsource": "^1.0.7",
|
"eventsource": "^1.0.7",
|
||||||
"fflate": "^0.7.0",
|
"fflate": "^0.7.0",
|
||||||
"fast-glob": "^3.2.5",
|
"fast-glob": "^3.2.5",
|
||||||
|
@ -703,7 +703,7 @@
|
||||||
"mssql": "^6.2.0",
|
"mssql": "^6.2.0",
|
||||||
"mysql2": "~2.3.0",
|
"mysql2": "~2.3.0",
|
||||||
"node-ssh": "^12.0.0",
|
"node-ssh": "^12.0.0",
|
||||||
"n8n-core": "~0.87.0",
|
"n8n-core": "~0.89.0",
|
||||||
"nodemailer": "^6.5.0",
|
"nodemailer": "^6.5.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"pg": "^8.3.0",
|
"pg": "^8.3.0",
|
||||||
|
@ -718,7 +718,7 @@
|
||||||
"ssh2-sftp-client": "^7.0.0",
|
"ssh2-sftp-client": "^7.0.0",
|
||||||
"tmp-promise": "^3.0.2",
|
"tmp-promise": "^3.0.2",
|
||||||
"uuid": "^8.3.0",
|
"uuid": "^8.3.0",
|
||||||
"vm2": "^3.6.10",
|
"vm2": "3.9.3",
|
||||||
"xlsx": "^0.17.0",
|
"xlsx": "^0.17.0",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-workflow",
|
"name": "n8n-workflow",
|
||||||
"version": "0.71.0",
|
"version": "0.72.0",
|
||||||
"description": "Workflow base code of n8n",
|
"description": "Workflow base code of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
|
|
@ -52,10 +52,12 @@ export type ExecutionError = WorkflowOperationError | NodeOperationError | NodeA
|
||||||
|
|
||||||
// Get used to gives nodes access to credentials
|
// Get used to gives nodes access to credentials
|
||||||
export interface IGetCredentials {
|
export interface IGetCredentials {
|
||||||
get(type: string, name: string): Promise<ICredentialsEncrypted>;
|
get(type: string, id: string | null): Promise<ICredentialsEncrypted>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ICredentials {
|
export abstract class ICredentials {
|
||||||
|
id?: string;
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -64,8 +66,15 @@ export abstract class ICredentials {
|
||||||
|
|
||||||
nodesAccess: ICredentialNodeAccess[];
|
nodesAccess: ICredentialNodeAccess[];
|
||||||
|
|
||||||
constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) {
|
constructor(
|
||||||
this.name = name;
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
|
type: string,
|
||||||
|
nodesAccess: ICredentialNodeAccess[],
|
||||||
|
data?: string,
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
this.id = nodeCredentials.id || undefined;
|
||||||
|
this.name = nodeCredentials.name;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.nodesAccess = nodesAccess;
|
this.nodesAccess = nodesAccess;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
@ -93,6 +102,7 @@ export interface ICredentialNodeAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialsDecrypted {
|
export interface ICredentialsDecrypted {
|
||||||
|
id: string | number;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
nodesAccess: ICredentialNodeAccess[];
|
nodesAccess: ICredentialNodeAccess[];
|
||||||
|
@ -100,6 +110,7 @@ export interface ICredentialsDecrypted {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialsEncrypted {
|
export interface ICredentialsEncrypted {
|
||||||
|
id?: string | number;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
nodesAccess: ICredentialNodeAccess[];
|
nodesAccess: ICredentialNodeAccess[];
|
||||||
|
@ -122,10 +133,13 @@ export abstract class ICredentialsHelper {
|
||||||
this.encryptionKey = encryptionKey;
|
this.encryptionKey = encryptionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getCredentials(name: string, type: string): Promise<ICredentials>;
|
abstract getCredentials(
|
||||||
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
|
type: string,
|
||||||
|
): Promise<ICredentials>;
|
||||||
|
|
||||||
abstract getDecrypted(
|
abstract getDecrypted(
|
||||||
name: string,
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
type: string,
|
type: string,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
raw?: boolean,
|
raw?: boolean,
|
||||||
|
@ -133,7 +147,7 @@ export abstract class ICredentialsHelper {
|
||||||
): Promise<ICredentialDataDecryptedObject>;
|
): Promise<ICredentialDataDecryptedObject>;
|
||||||
|
|
||||||
abstract updateCredentials(
|
abstract updateCredentials(
|
||||||
name: string,
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
type: string,
|
type: string,
|
||||||
data: ICredentialDataDecryptedObject,
|
data: ICredentialDataDecryptedObject,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
@ -160,6 +174,7 @@ export interface ICredentialTypes {
|
||||||
|
|
||||||
// The way the credentials get saved in the database (data encrypted)
|
// The way the credentials get saved in the database (data encrypted)
|
||||||
export interface ICredentialData {
|
export interface ICredentialData {
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
data: string; // Contains the access data as encrypted JSON string
|
data: string; // Contains the access data as encrypted JSON string
|
||||||
nodesAccess: ICredentialNodeAccess[];
|
nodesAccess: ICredentialNodeAccess[];
|
||||||
|
@ -356,7 +371,7 @@ export interface IExecuteFunctions {
|
||||||
outputIndex?: number,
|
outputIndex?: number,
|
||||||
): Promise<INodeExecutionData[][]>;
|
): Promise<INodeExecutionData[][]>;
|
||||||
putExecutionToWait(waitTill: Date): Promise<void>;
|
putExecutionToWait(waitTill: Date): Promise<void>;
|
||||||
sendMessageToUI(message: any): void; // tslint:disable-line:no-any
|
sendMessageToUI(message: any): void;
|
||||||
helpers: {
|
helpers: {
|
||||||
httpRequest(
|
httpRequest(
|
||||||
requestOptions: IHttpRequestOptions,
|
requestOptions: IHttpRequestOptions,
|
||||||
|
@ -529,8 +544,13 @@ export interface IWebhookFunctions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INodeCredentialsDetails {
|
||||||
|
id: string | null;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface INodeCredentials {
|
export interface INodeCredentials {
|
||||||
[key: string]: string;
|
[key: string]: INodeCredentialsDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
|
@ -942,10 +962,8 @@ export interface IWorkflowBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowCredentials {
|
export interface IWorkflowCredentials {
|
||||||
// Credential type
|
[credentialType: string]: {
|
||||||
[key: string]: {
|
[id: string]: ICredentialsEncrypted;
|
||||||
// Name
|
|
||||||
[key: string]: ICredentialsEncrypted;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue