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
131 changes: 103 additions & 28 deletions packages/cli/commands/import/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/* 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 +13,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 { 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,8 +43,13 @@ 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',
}),
};

ownerWorkflowRole: Role;

private transformCredentials(node: INode, credentialsEntities: ICredentialsDb[]) {
if (node.credentials) {
const allNodeCredentials = Object.entries(node.credentials);
Expand All @@ -55,23 +76,21 @@ export class ImportWorkflowsCommand extends Command {
}
}

// 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,10 +99,13 @@ 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 i: number;
if (flags.separate) {
let inputPath = flags.input;
if (process.platform === 'win32') {
Expand All @@ -93,41 +115,94 @@ export class ImportWorkflowsCommand extends Command {
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
if (credentials.length > 0) {
workflow.nodes.forEach((node: INode) => {
this.transformCredentials(node, credentialsEntities);
this.transformCredentials(node, credentials);
});
}
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
await Db.collections.Workflow!.save(workflow);
await this.storeWorkflow(workflow, user);
}
} else {
const fileContents = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
const workflows = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));

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

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 (i = 0; i < workflows.length; i++) {
if (credentials.length > 0) {
workflows[i].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(workflows[i], user);
}
ivov marked this conversation as resolved.
Show resolved Hide resolved
}

console.info(`Successfully imported ${i} ${i === 1 ? 'workflow.' : 'workflows.'}`);
process.exit(0);
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 async initOwnerWorkflowRole() {
const ownerWorkflowRole = await Db.collections.Role!.findOne({
where: { name: 'owner', scope: 'workflow' },
});

if (!ownerWorkflowRole) {
throw new Error(`Owner workflow role not found. ${FIX_INSTRUCTION}`);
}

this.ownerWorkflowRole = ownerWorkflowRole;
}

private async storeWorkflow(workflow: object, user: User) {
await getConnection().transaction(async (transactionManager) => {
const newWorkflow = new WorkflowEntity();

Object.assign(newWorkflow, workflow);

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

const newSharedWorkflow = new SharedWorkflow();

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

await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
});
ivov marked this conversation as resolved.
Show resolved Hide resolved
}

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(`No owner found. ${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}. Are you sure this user exists?`);
ivov marked this conversation as resolved.
Show resolved Hide resolved
}

return user;
}
}
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