Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1327-…
Browse files Browse the repository at this point in the history
…run-once-for-each-item-tooltip
  • Loading branch information
michael-radency committed Jun 4, 2024
2 parents 40ffb02 + 631f077 commit c5bd899
Show file tree
Hide file tree
Showing 100 changed files with 1,433 additions and 699 deletions.
7 changes: 5 additions & 2 deletions packages/@n8n/permissions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export type CommunityPackageScope = ResourceScope<
'communityPackage',
'install' | 'uninstall' | 'update' | 'list' | 'manage'
>;
export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share'>;
export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share' | 'move'>;
export type ExternalSecretScope = ResourceScope<'externalSecret', 'list' | 'use'>;
export type ExternalSecretProviderScope = ResourceScope<
'externalSecretsProvider',
Expand All @@ -58,7 +58,10 @@ export type TagScope = ResourceScope<'tag'>;
export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword' | 'changeRole'>;
export type VariableScope = ResourceScope<'variable'>;
export type WorkersViewScope = ResourceScope<'workersView', 'manage'>;
export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share' | 'execute'>;
export type WorkflowScope = ResourceScope<
'workflow',
DefaultOperations | 'share' | 'execute' | 'move'
>;

export type Scope =
| AuditLogsScope
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/WaitTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ export class WaitTracker {
private readonly executionRepository: ExecutionRepository,
private readonly ownershipService: OwnershipService,
private readonly workflowRunner: WorkflowRunner,
readonly orchestrationService: OrchestrationService,
) {
const { isSingleMainSetup, isLeader, multiMainSetup } = orchestrationService;
private readonly orchestrationService: OrchestrationService,
) {}

init() {
const { isSingleMainSetup, isLeader, multiMainSetup } = this.orchestrationService;

if (isSingleMainSetup) {
this.startTracking();
Expand All @@ -43,7 +45,7 @@ export class WaitTracker {
.on('leader-stepdown', () => this.stopTracking());
}

startTracking() {
private startTracking() {
this.logger.debug('Wait tracker started tracking waiting executions');

// Poll every 60 seconds a list of upcoming executions
Expand Down
38 changes: 0 additions & 38 deletions packages/cli/src/WorkflowRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import PCancelable from 'p-cancelable';
import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { ExecutionDataRecoveryService } from '@/eventbus/executionDataRecovery.service';
import { ExternalHooks } from '@/ExternalHooks';
import type { IExecutionResponse, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
Expand Down Expand Up @@ -103,42 +101,6 @@ export class WorkflowRunner {
status: 'error',
};

// The following will attempt to recover runData from event logs
// Note that this will only work as long as the event logs actually contain the events from this workflow execution
// Since processError is run almost immediately after the workflow execution has failed, it is likely that the event logs
// does contain those messages.
try {
// Search for messages for this executionId in event logs
const eventBus = Container.get(MessageEventBus);
const eventLogMessages = await eventBus.getEventsByExecutionId(executionId);
// Attempt to recover more better runData from these messages (but don't update the execution db entry yet)
if (eventLogMessages.length > 0) {
const eventLogExecutionData = await Container.get(
ExecutionDataRecoveryService,
).recoverExecutionData(executionId, eventLogMessages, false);
if (eventLogExecutionData) {
fullRunData.data.resultData.runData = eventLogExecutionData.resultData.runData;
fullRunData.status = 'crashed';
}
}

const executionFlattedData = await this.executionRepository.findSingleExecution(executionId, {
includeData: true,
});

if (executionFlattedData) {
void Container.get(InternalHooks).onWorkflowCrashed(
executionId,
executionMode,
executionFlattedData?.workflowData,
// TODO: get metadata to be sent here
// executionFlattedData?.metadata,
);
}
} catch {
// Ignore errors
}

// Remove from active execution with empty data. That will
// set the execution to failed.
this.activeExecutions.remove(executionId, fullRunData);
Expand Down
6 changes: 0 additions & 6 deletions packages/cli/src/commands/import/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { EntityManager } from '@n8n/typeorm';
import * as Db from '@/Db';
import { SharedCredentials } from '@db/entities/SharedCredentials';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import { BaseCommand } from '../BaseCommand';
import type { ICredentialsEncrypted } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
Expand Down Expand Up @@ -47,11 +46,6 @@ export class ImportCredentialsCommand extends BaseCommand {

private transactionManager: EntityManager;

async init() {
disableAutoGeneratedIds(CredentialsEntity);
await super.init();
}

async run(): Promise<void> {
const { flags } = await this.parse(ImportCredentialsCommand);

Expand Down
8 changes: 1 addition & 7 deletions packages/cli/src/commands/import/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import fs from 'fs';
import glob from 'fast-glob';

import { UM_FIX_INSTRUCTION } from '@/constants';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { generateNanoId } from '@db/utils/generators';
import { UserRepository } from '@db/repositories/user.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
Expand Down Expand Up @@ -62,11 +61,6 @@ export class ImportWorkflowsCommand extends BaseCommand {
}),
};

async init() {
disableAutoGeneratedIds(WorkflowEntity);
await super.init();
}

async run(): Promise<void> {
const { flags } = await this.parse(ImportWorkflowsCommand);

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export class Start extends BaseCommand {

await this.initOrchestration();
this.logger.debug('Orchestration init complete');
Container.get(WaitTracker).init();
this.logger.debug('Wait tracker init complete');
await this.initBinaryDataService();
this.logger.debug('Binary data service init complete');
await this.initExternalHooks();
Expand Down
11 changes: 0 additions & 11 deletions packages/cli/src/databases/utils/commandHelpers.ts

This file was deleted.

10 changes: 10 additions & 0 deletions packages/cli/src/errors/response-errors/not-found.error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { ResponseError } from './abstract/response.error';

export class NotFoundError extends ResponseError {
static isDefinedAndNotNull<T>(
value: T | undefined | null,
message: string,
hint?: string,
): asserts value is T {
if (value === undefined || value === null) {
throw new NotFoundError(message, hint);
}
}

constructor(message: string, hint: string | undefined = undefined) {
super(message, 404, 404, hint);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ResponseError } from './abstract/response.error';

export class TransferWorkflowError extends ResponseError {
constructor(message: string) {
super(message, 400, 400);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/permissions/global-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [
'credential:delete',
'credential:list',
'credential:share',
'credential:move',
'communityPackage:install',
'communityPackage:uninstall',
'communityPackage:update',
Expand Down Expand Up @@ -68,6 +69,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [
'workflow:list',
'workflow:share',
'workflow:execute',
'workflow:move',
'workersView:manage',
'project:list',
'project:create',
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/permissions/project-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [
'workflow:delete',
'workflow:list',
'workflow:execute',
'workflow:move',
'credential:create',
'credential:read',
'credential:update',
'credential:delete',
'credential:list',
'credential:move',
'project:list',
'project:read',
'project:update',
Expand All @@ -32,12 +34,14 @@ export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
'workflow:list',
'workflow:execute',
'workflow:share',
'workflow:move',
'credential:create',
'credential:read',
'credential:update',
'credential:delete',
'credential:list',
'credential:share',
'credential:move',
'project:list',
'project:read',
];
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/permissions/resource-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const CREDENTIALS_SHARING_OWNER_SCOPES: Scope[] = [
'credential:update',
'credential:delete',
'credential:share',
'credential:move',
];

export const CREDENTIALS_SHARING_USER_SCOPES: Scope[] = ['credential:read'];
Expand All @@ -15,6 +16,7 @@ export const WORKFLOW_SHARING_OWNER_SCOPES: Scope[] = [
'workflow:delete',
'workflow:execute',
'workflow:share',
'workflow:move',
];

export const WORKFLOW_SHARING_EDITOR_SCOPES: Scope[] = [
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/workflows/workflow.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,11 @@ export declare namespace WorkflowRequest {

type Share = AuthenticatedRequest<{ workflowId: string }, {}, { shareWithIds: string[] }>;

type Transfer = AuthenticatedRequest<
{ workflowId: string },
{},
{ destinationProjectId: string }
>;

type FromUrl = AuthenticatedRequest<{}, {}, {}, { url?: string }>;
}
104 changes: 103 additions & 1 deletion packages/cli/src/workflows/workflow.service.ee.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Service } from 'typedi';
import omit from 'lodash/omit';
import { ApplicationError, NodeOperationError } from 'n8n-workflow';
import { ApplicationError, NodeOperationError, WorkflowActivationError } from 'n8n-workflow';

import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { User } from '@db/entities/User';
Expand All @@ -20,6 +20,10 @@ import type {
import { OwnershipService } from '@/services/ownership.service';
import { In, type EntityManager } from '@n8n/typeorm';
import { Project } from '@/databases/entities/Project';
import { ProjectService } from '@/services/project.service';
import { ActiveWorkflowManager } from '@/ActiveWorkflowManager';
import { TransferWorkflowError } from '@/errors/response-errors/transfer-workflow.error';
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';

@Service()
export class EnterpriseWorkflowService {
Expand All @@ -30,6 +34,8 @@ export class EnterpriseWorkflowService {
private readonly credentialsRepository: CredentialsRepository,
private readonly credentialsService: CredentialsService,
private readonly ownershipService: OwnershipService,
private readonly projectService: ProjectService,
private readonly activeWorkflowManager: ActiveWorkflowManager,
) {}

async shareWithProjects(
Expand Down Expand Up @@ -235,4 +241,100 @@ export class EnterpriseWorkflowService {
);
});
}

async transferOne(user: User, workflowId: string, destinationProjectId: string) {
// 1. get workflow
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
'workflow:move',
]);
NotFoundError.isDefinedAndNotNull(
workflow,
`Could not find workflow with the id "${workflowId}". Make sure you have the permission to delete it.`,
);

// 2. get owner-sharing
const ownerSharing = workflow.shared.find((s) => s.role === 'workflow:owner')!;
NotFoundError.isDefinedAndNotNull(
ownerSharing,
`Could not find owner for workflow ${workflow.id}`,
);

// 3. get source project
const sourceProject = ownerSharing.project;

// 4. get destination project
const destinationProject = await this.projectService.getProjectWithScope(
user,
destinationProjectId,
['workflow:create'],
);
NotFoundError.isDefinedAndNotNull(
destinationProject,
`Could not find project with the id "${destinationProjectId}". Make sure you have the permission to create workflows in it.`,
);

// 5. checks
if (sourceProject.id === destinationProject.id) {
throw new TransferWorkflowError(
"You can't transfer a workflow into the project that's already owning it.",
);
}
if (sourceProject.type !== 'team' && sourceProject.type !== 'personal') {
throw new TransferWorkflowError(
'You can only transfer workflows out of personal or team projects.',
);
}
if (destinationProject.type !== 'team') {
throw new TransferWorkflowError('You can only transfer workflows into team projects.');
}

// 6. deactivate workflow if necessary
const wasActive = workflow.active;
if (wasActive) {
await this.activeWorkflowManager.remove(workflowId);
}

// 7. transfer the workflow
await this.workflowRepository.manager.transaction(async (trx) => {
// remove all sharings
await trx.remove(workflow.shared);

// create new owner-sharing
await trx.save(
trx.create(SharedWorkflow, {
workflowId: workflow.id,
projectId: destinationProject.id,
role: 'workflow:owner',
}),
);
});

// 8. try to activate it again if it was active
if (wasActive) {
try {
await this.activeWorkflowManager.add(workflowId, 'update');

return;
} catch (error) {
await this.workflowRepository.updateActiveState(workflowId, false);

// Since the transfer worked we return a 200 but also return the
// activation error as data.
if (error instanceof WorkflowActivationError) {
return {
error: error.toJSON
? error.toJSON()
: {
name: error.name,
message: error.message,
},
};
}

throw error;
}
}

return;
}
}
Loading

0 comments on commit c5bd899

Please sign in to comment.