Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update import workflows command for user management #2701

Merged
Merged
228 changes: 166 additions & 62 deletions packages/cli/commands/import/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-loop-func */
/* eslint-disable no-await-in-loop */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Command, flags } from '@oclif/command';
Expand All @@ -7,15 +15,25 @@ import { INode, INodeCredentialsDetails, LoggerProxy } from 'n8n-workflow';
import * as fs from 'fs';
import * as glob from 'fast-glob';
import { UserSettings } from 'n8n-core';
import { EntityManager, getConnection } from 'typeorm';
import { getLogger } from '../../src/Logger';
import { Db, ICredentialsDb } from '../../src';
import { SharedWorkflow } from '../../src/databases/entities/SharedWorkflow';
import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
import { Role } from '../../src/databases/entities/Role';
import { User } from '../../src/databases/entities/User';

const FIX_INSTRUCTION =
'Please fix the database by running ./packages/cli/bin/n8n user-management:reset';

export class ImportWorkflowsCommand extends Command {
static description = 'Import workflows';

static examples = [
`$ n8n import:workflow --input=file.json`,
`$ n8n import:workflow --separate --input=backups/latest/`,
'$ n8n import:workflow --input=file.json',
'$ n8n import:workflow --separate --input=backups/latest/',
'$ n8n import:workflow --input=file.json --id=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
'$ n8n import:workflow --separate --input=backups/latest/ --id=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
ivov marked this conversation as resolved.
Show resolved Hide resolved
];

static flags = {
Expand All @@ -27,51 +45,30 @@ export class ImportWorkflowsCommand extends Command {
separate: flags.boolean({
description: 'Imports *.json files from directory provided by --input',
}),
id: flags.string({
ivov marked this conversation as resolved.
Show resolved Hide resolved
description: 'The ID of the user to assign the imported workflows to',
}),
};

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,
};
ownerWorkflowRole: Role;

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;
}
}
}
}
transactionManager: EntityManager;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async run() {
async run(): Promise<void> {
const logger = getLogger();
LoggerProxy.init(logger);

// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(ImportWorkflowsCommand);

if (!flags.input) {
console.info(`An input file or directory with --input must be provided`);
console.info('An input file or directory with --input must be provided');
return;
}

if (flags.separate) {
if (fs.existsSync(flags.input)) {
if (!fs.lstatSync(flags.input).isDirectory()) {
console.info(`The paramenter --input must be a directory`);
console.info('The argument to --input must be a directory');
return;
}
}
Expand All @@ -80,54 +77,161 @@ export class ImportWorkflowsCommand extends Command {
try {
await Db.init();

await this.initOwnerWorkflowRole();
const user = flags.id ? await this.getAssignee(flags.id) : await this.getOwner();
ivov marked this conversation as resolved.
Show resolved Hide resolved

// Make sure the settings exist
await UserSettings.prepareUserSettings();
const credentialsEntities = (await Db.collections.Credentials?.find()) ?? [];
let i;
const credentials = (await Db.collections.Credentials?.find()) ?? [];

let totalImported = 0;

if (flags.separate) {
let inputPath = flags.input;
let { input: inputPath } = flags;

if (process.platform === 'win32') {
inputPath = inputPath.replace(/\\/g, '/');
}

inputPath = inputPath.replace(/\/$/g, '');

const files = await glob(`${inputPath}/*.json`);
for (i = 0; i < files.length; i++) {
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);
});

totalImported = files.length;

await getConnection().transaction(async (transactionManager) => {
this.transactionManager = transactionManager;

for (const file of files) {
const workflow = JSON.parse(fs.readFileSync(file, { encoding: 'utf8' }));

if (credentials.length > 0) {
workflow.nodes.forEach((node: INode) => {
this.transformCredentials(node, credentials);
});
}

await this.storeWorkflow(workflow, user);
}
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
await Db.collections.Workflow!.save(workflow);
}
} else {
const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
});

if (!Array.isArray(fileContents)) {
throw new Error(`File does not seem to contain workflows.`);
}
this.reportSuccess(totalImported);
process.exit();
}

const workflows = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));

totalImported = workflows.length;

if (!Array.isArray(workflows)) {
throw new Error(
'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
);
}

await getConnection().transaction(async (transactionManager) => {
this.transactionManager = transactionManager;

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);
for (const workflow of workflows) {
if (credentials.length > 0) {
workflow.nodes.forEach((node: INode) => {
this.transformCredentials(node, credentials);
});
}
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
await Db.collections.Workflow!.save(fileContents[i]);

await this.storeWorkflow(workflow, user);
}
}
});

console.info(`Successfully imported ${i} ${i === 1 ? 'workflow.' : 'workflows.'}`);
process.exit(0);
this.reportSuccess(totalImported);
process.exit();
} catch (error) {
console.error('An error occurred while exporting workflows. See log messages for details.');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
logger.error(error.message);
console.error('An error occurred while importing workflows. See log messages for details.');
if (error instanceof Error) logger.error(error.message);
this.exit(1);
}
}

private reportSuccess(total: number) {
console.info(`Successfully imported ${total} ${total === 1 ? 'workflow.' : 'workflows.'}`);
}

private async initOwnerWorkflowRole() {
const ownerWorkflowRole = await Db.collections.Role!.findOne({
where: { name: 'owner', scope: 'workflow' },
});

if (!ownerWorkflowRole) {
throw new Error(`Failed to find owner workflow role. ${FIX_INSTRUCTION}`);
}

this.ownerWorkflowRole = ownerWorkflowRole;
}

private async storeWorkflow(workflow: object, user: User) {
const newWorkflow = new WorkflowEntity();

Object.assign(newWorkflow, workflow);

const savedWorkflow = await this.transactionManager.save<WorkflowEntity>(newWorkflow);

const newSharedWorkflow = new SharedWorkflow();

Object.assign(newSharedWorkflow, {
workflow: savedWorkflow,
user,
role: this.ownerWorkflowRole,
});

await this.transactionManager.save<SharedWorkflow>(newSharedWorkflow);
}

private async getOwner() {
const ownerGlobalRole = await Db.collections.Role!.findOne({
where: { name: 'owner', scope: 'global' },
});

const owner = await Db.collections.User!.findOne({ globalRole: ownerGlobalRole });

if (!owner) {
throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`);
}

return owner;
}

private async getAssignee(id: string) {
const user = await Db.collections.User!.findOne(id);

if (!user) {
throw new Error(`Failed to find user with ID ${id}`);
}

return user;
}

private transformCredentials(node: INode, credentialsEntities: ICredentialsDb[]) {
if (node.credentials) {
const allNodeCredentials = Object.entries(node.credentials);
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;
}
}
}
}
}
1 change: 1 addition & 0 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,7 @@ class App {
}

await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
// @ts-ignore
void InternalHooksManager.getInstance().onWorkflowSaved(updatedWorkflow as IWorkflowBase);

if (updatedWorkflow.active) {
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/UserManagement/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { Application } from 'express';
import express = require('express');
import { JwtFromRequestFunction } from 'passport-jwt';
import { Interface } from 'readline';
import { User } from '../databases/entities/User';

export interface JwtToken {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/UserManagement/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Request, Response } from 'express';
import { getConnection, In } from 'typeorm';
import { LoggerProxy } from 'n8n-workflow';
import { genSaltSync, hashSync } from 'bcryptjs';
import { Db, GenericHelpers, ICredentialsResponse, ResponseHelper } from '../..';
import { Db, GenericHelpers, ResponseHelper } from '../..';
import { AuthenticatedRequest, N8nApp, UserRequest } from '../Interfaces';
import { isEmailSetup, isValidEmail, sanitizeUser } from '../UserManagementHelper';
import { User } from '../../databases/entities/User';
Expand Down