2023-02-21 10:21:56 -08:00
|
|
|
import { Container } from 'typedi';
|
2023-09-05 04:42:31 -07:00
|
|
|
import type { IDataObject, INode, IPinData } from 'n8n-workflow';
|
2023-10-25 07:35:22 -07:00
|
|
|
import { NodeApiError, ErrorReporterProxy as ErrorReporter, Workflow } from 'n8n-workflow';
|
2023-08-09 03:30:02 -07:00
|
|
|
import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere, UpdateResult } from 'typeorm';
|
|
|
|
import { In, Like } from 'typeorm';
|
2023-06-16 07:26:35 -07:00
|
|
|
import pick from 'lodash/pick';
|
2022-12-06 00:25:39 -08:00
|
|
|
import { v4 as uuid } from 'uuid';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
2022-11-09 06:25:00 -08:00
|
|
|
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
|
|
|
import config from '@/config';
|
2023-01-27 05:56:56 -08:00
|
|
|
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
|
|
|
import type { User } from '@db/entities/User';
|
|
|
|
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
2022-11-09 06:25:00 -08:00
|
|
|
import { validateEntity } from '@/GenericHelpers';
|
2022-12-21 01:46:26 -08:00
|
|
|
import { ExternalHooks } from '@/ExternalHooks';
|
2023-08-22 04:19:37 -07:00
|
|
|
import { type WorkflowRequest, type ListQuery, hasSharing } from '@/requests';
|
2023-08-22 03:24:43 -07:00
|
|
|
import { TagService } from '@/services/tag.service';
|
2023-01-02 08:42:32 -08:00
|
|
|
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
|
2022-11-11 02:14:45 -08:00
|
|
|
import { NodeTypes } from '@/NodeTypes';
|
|
|
|
import { WorkflowRunner } from '@/WorkflowRunner';
|
|
|
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { TestWebhooks } from '@/TestWebhooks';
|
2023-08-22 04:19:37 -07:00
|
|
|
import { whereClause } from '@/UserManagement/UserManagementHelper';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { InternalHooks } from '@/InternalHooks';
|
2023-11-10 06:04:26 -08:00
|
|
|
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
2023-08-22 04:19:37 -07:00
|
|
|
import { RoleService } from '@/services/role.service';
|
|
|
|
import { OwnershipService } from '@/services/ownership.service';
|
2023-09-05 04:42:31 -07:00
|
|
|
import { isStringArray, isWorkflowIdValid } from '@/utils';
|
2023-09-27 07:22:39 -07:00
|
|
|
import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee';
|
2023-10-10 01:06:06 -07:00
|
|
|
import { BinaryDataService } from 'n8n-core';
|
2023-10-25 07:35:22 -07:00
|
|
|
import { Logger } from '@/Logger';
|
2023-11-17 06:58:50 -08:00
|
|
|
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
|
2023-11-10 06:04:26 -08:00
|
|
|
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
|
|
|
import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository';
|
|
|
|
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
2023-11-28 01:19:27 -08:00
|
|
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
|
|
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
2022-10-11 05:55:05 -07:00
|
|
|
|
|
|
|
export class WorkflowsService {
|
|
|
|
static async getSharing(
|
|
|
|
user: User,
|
2023-01-02 08:42:32 -08:00
|
|
|
workflowId: string,
|
2022-10-11 05:55:05 -07:00
|
|
|
relations: string[] = ['workflow'],
|
|
|
|
{ allowGlobalOwner } = { allowGlobalOwner: true },
|
2023-01-13 09:12:22 -08:00
|
|
|
): Promise<SharedWorkflow | null> {
|
|
|
|
const where: FindOptionsWhere<SharedWorkflow> = { workflowId };
|
2022-10-11 05:55:05 -07:00
|
|
|
|
|
|
|
// Omit user from where if the requesting user is the global
|
|
|
|
// owner. This allows the global owner to view and delete
|
|
|
|
// workflows they don't own.
|
|
|
|
if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
|
2023-01-02 08:42:32 -08:00
|
|
|
where.userId = user.id;
|
2022-10-11 05:55:05 -07:00
|
|
|
}
|
|
|
|
|
2023-11-10 06:04:26 -08:00
|
|
|
return Container.get(SharedWorkflowRepository).findOne({ where, relations });
|
2022-10-11 05:55:05 -07:00
|
|
|
}
|
2022-10-11 07:40:39 -07:00
|
|
|
|
2022-11-10 05:03:14 -08:00
|
|
|
/**
|
|
|
|
* Find the pinned trigger to execute the workflow from, if any.
|
2022-12-05 01:09:31 -08:00
|
|
|
*
|
|
|
|
* - In a full execution, select the _first_ pinned trigger.
|
|
|
|
* - In a partial execution,
|
|
|
|
* - select the _first_ pinned trigger that leads to the executed node,
|
|
|
|
* - else select the executed pinned trigger.
|
2022-11-10 05:03:14 -08:00
|
|
|
*/
|
|
|
|
static findPinnedTrigger(workflow: IWorkflowDb, startNodes?: string[], pinData?: IPinData) {
|
|
|
|
if (!pinData || !startNodes) return null;
|
|
|
|
|
|
|
|
const isTrigger = (nodeTypeName: string) =>
|
|
|
|
['trigger', 'webhook'].some((suffix) => nodeTypeName.toLowerCase().includes(suffix));
|
|
|
|
|
|
|
|
const pinnedTriggers = workflow.nodes.filter(
|
|
|
|
(node) => !node.disabled && pinData[node.name] && isTrigger(node.type),
|
|
|
|
);
|
|
|
|
|
|
|
|
if (pinnedTriggers.length === 0) return null;
|
|
|
|
|
|
|
|
if (startNodes?.length === 0) return pinnedTriggers[0]; // full execution
|
|
|
|
|
|
|
|
const [startNodeName] = startNodes;
|
|
|
|
|
2022-12-05 01:09:31 -08:00
|
|
|
const parentNames = new Workflow({
|
|
|
|
nodes: workflow.nodes,
|
|
|
|
connections: workflow.connections,
|
|
|
|
active: workflow.active,
|
2023-02-21 10:21:56 -08:00
|
|
|
nodeTypes: Container.get(NodeTypes),
|
2022-12-05 01:09:31 -08:00
|
|
|
}).getParentNodes(startNodeName);
|
|
|
|
|
|
|
|
let checkNodeName = '';
|
|
|
|
|
|
|
|
if (parentNames.length === 0) {
|
|
|
|
checkNodeName = startNodeName;
|
|
|
|
} else {
|
|
|
|
checkNodeName = parentNames.find((pn) => pn === pinnedTriggers[0].name) as string;
|
|
|
|
}
|
|
|
|
|
|
|
|
return pinnedTriggers.find((pt) => pt.name === checkNodeName) ?? null; // partial execution
|
2022-11-10 05:03:14 -08:00
|
|
|
}
|
|
|
|
|
2023-01-13 09:12:22 -08:00
|
|
|
static async get(workflow: FindOptionsWhere<WorkflowEntity>, options?: { relations: string[] }) {
|
2023-11-10 06:04:26 -08:00
|
|
|
return Container.get(WorkflowRepository).findOne({
|
|
|
|
where: workflow,
|
|
|
|
relations: options?.relations,
|
|
|
|
});
|
2022-10-11 07:40:39 -07:00
|
|
|
}
|
2022-10-26 06:49:43 -07:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
static async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) {
|
|
|
|
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
|
2022-11-18 04:07:39 -08:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
const where: FindOptionsWhere<WorkflowEntity> = {
|
|
|
|
...options?.filter,
|
|
|
|
id: In(sharedWorkflowIds),
|
|
|
|
};
|
2022-11-08 08:52:42 -08:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
const reqTags = options?.filter?.tags;
|
2022-11-08 08:52:42 -08:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
if (isStringArray(reqTags)) {
|
|
|
|
where.tags = reqTags.map((tag) => ({ name: tag }));
|
2022-11-08 08:52:42 -08:00
|
|
|
}
|
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
type Select = FindOptionsSelect<WorkflowEntity> & { ownedBy?: true };
|
|
|
|
|
|
|
|
const select: Select = options?.select
|
|
|
|
? { ...options.select } // copy to enable field removal without affecting original
|
|
|
|
: {
|
|
|
|
name: true,
|
|
|
|
active: true,
|
|
|
|
createdAt: true,
|
|
|
|
updatedAt: true,
|
|
|
|
versionId: true,
|
|
|
|
shared: { userId: true, roleId: true },
|
|
|
|
};
|
2023-08-09 03:30:02 -07:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
delete select?.ownedBy; // remove non-entity field, handled after query
|
2023-08-09 03:30:02 -07:00
|
|
|
|
2022-11-25 05:20:28 -08:00
|
|
|
const relations: string[] = [];
|
2022-11-08 08:52:42 -08:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
const areTagsEnabled = !config.getEnv('workflowTagsDisabled');
|
2023-08-09 03:30:02 -07:00
|
|
|
const isDefaultSelect = options?.select === undefined;
|
2023-08-22 04:19:37 -07:00
|
|
|
const areTagsRequested = isDefaultSelect || options?.select?.tags === true;
|
|
|
|
const isOwnedByIncluded = isDefaultSelect || options?.select?.ownedBy === true;
|
2023-08-09 03:30:02 -07:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
if (areTagsEnabled && areTagsRequested) {
|
2022-11-25 05:20:28 -08:00
|
|
|
relations.push('tags');
|
2023-02-27 03:25:45 -08:00
|
|
|
select.tags = { id: true, name: true };
|
2022-11-25 05:20:28 -08:00
|
|
|
}
|
2022-11-08 08:52:42 -08:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
if (isOwnedByIncluded) relations.push('shared');
|
2023-08-09 03:30:02 -07:00
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
if (typeof where.name === 'string' && where.name !== '') {
|
|
|
|
where.name = Like(`%${where.name}%`);
|
2023-08-09 03:30:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const findManyOptions: FindManyOptions<WorkflowEntity> = {
|
2023-08-22 04:19:37 -07:00
|
|
|
select: { ...select, id: true },
|
|
|
|
where,
|
2023-08-09 03:30:02 -07:00
|
|
|
};
|
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
if (isDefaultSelect || options?.select?.updatedAt === true) {
|
|
|
|
findManyOptions.order = { updatedAt: 'ASC' };
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relations.length > 0) {
|
|
|
|
findManyOptions.relations = relations;
|
|
|
|
}
|
|
|
|
|
2023-08-09 03:30:02 -07:00
|
|
|
if (options?.take) {
|
|
|
|
findManyOptions.skip = options.skip;
|
|
|
|
findManyOptions.take = options.take;
|
|
|
|
}
|
|
|
|
|
2023-08-22 04:19:37 -07:00
|
|
|
const [workflows, count] = (await Container.get(WorkflowRepository).findAndCount(
|
|
|
|
findManyOptions,
|
|
|
|
)) as [ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[], number];
|
|
|
|
|
|
|
|
if (!hasSharing(workflows)) return { workflows, count };
|
|
|
|
|
|
|
|
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
|
|
|
|
|
|
|
|
return {
|
|
|
|
workflows: workflows.map((w) =>
|
|
|
|
Container.get(OwnershipService).addOwnedBy(w, workflowOwnerRole),
|
|
|
|
),
|
|
|
|
count,
|
|
|
|
};
|
2022-11-08 08:52:42 -08:00
|
|
|
}
|
|
|
|
|
2022-11-11 02:14:45 -08:00
|
|
|
static async update(
|
2022-10-26 06:49:43 -07:00
|
|
|
user: User,
|
|
|
|
workflow: WorkflowEntity,
|
|
|
|
workflowId: string,
|
2023-03-30 07:25:51 -07:00
|
|
|
tagIds?: string[],
|
2022-10-31 02:35:24 -07:00
|
|
|
forceSave?: boolean,
|
2022-11-18 04:07:39 -08:00
|
|
|
roles?: string[],
|
2022-10-26 06:49:43 -07:00
|
|
|
): Promise<WorkflowEntity> {
|
2023-11-10 06:04:26 -08:00
|
|
|
const shared = await Container.get(SharedWorkflowRepository).findOne({
|
2022-11-18 04:07:39 -08:00
|
|
|
relations: ['workflow', 'role'],
|
2022-10-26 06:49:43 -07:00
|
|
|
where: whereClause({
|
|
|
|
user,
|
|
|
|
entityType: 'workflow',
|
|
|
|
entityId: workflowId,
|
2022-11-18 04:07:39 -08:00
|
|
|
roles,
|
2022-10-26 06:49:43 -07:00
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
2023-10-25 07:35:22 -07:00
|
|
|
const logger = Container.get(Logger);
|
2022-10-26 06:49:43 -07:00
|
|
|
if (!shared) {
|
2023-10-25 07:35:22 -07:00
|
|
|
logger.verbose('User attempted to update a workflow without permissions', {
|
2022-10-26 06:49:43 -07:00
|
|
|
workflowId,
|
|
|
|
userId: user.id,
|
|
|
|
});
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new NotFoundError(
|
2022-11-22 04:05:51 -08:00
|
|
|
'You do not have permission to update this workflow. Ask the owner to share it with you.',
|
2022-10-26 06:49:43 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-11-17 06:58:50 -08:00
|
|
|
const oldState = shared.workflow.active;
|
|
|
|
|
2022-12-06 00:25:39 -08:00
|
|
|
if (
|
|
|
|
!forceSave &&
|
|
|
|
workflow.versionId !== '' &&
|
|
|
|
workflow.versionId !== shared.workflow.versionId
|
|
|
|
) {
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new BadRequestError(
|
2022-11-28 12:05:19 -08:00
|
|
|
'Your most recent changes may be lost, because someone else just updated this workflow. Open this workflow in a new tab to see those new updates.',
|
2022-12-06 00:25:39 -08:00
|
|
|
100,
|
2022-11-14 06:38:19 -08:00
|
|
|
);
|
|
|
|
}
|
2022-10-31 02:35:24 -07:00
|
|
|
|
2023-10-25 03:07:11 -07:00
|
|
|
let onlyActiveUpdate = false;
|
|
|
|
|
2023-07-26 00:25:01 -07:00
|
|
|
if (
|
2023-10-25 03:07:11 -07:00
|
|
|
(Object.keys(workflow).length === 3 &&
|
|
|
|
workflow.id !== undefined &&
|
|
|
|
workflow.versionId !== undefined &&
|
|
|
|
workflow.active !== undefined) ||
|
|
|
|
(Object.keys(workflow).length === 2 &&
|
|
|
|
workflow.versionId !== undefined &&
|
|
|
|
workflow.active !== undefined)
|
2023-07-26 00:25:01 -07:00
|
|
|
) {
|
|
|
|
// we're just updating the active status of the workflow, don't update the versionId
|
2023-10-25 03:07:11 -07:00
|
|
|
onlyActiveUpdate = true;
|
2023-07-26 00:25:01 -07:00
|
|
|
} else {
|
|
|
|
// Update the workflow's version
|
|
|
|
workflow.versionId = uuid();
|
2023-10-25 07:35:22 -07:00
|
|
|
logger.verbose(
|
2023-07-26 00:25:01 -07:00
|
|
|
`Updating versionId for workflow ${workflowId} for user ${user.id} after saving`,
|
|
|
|
{
|
|
|
|
previousVersionId: shared.workflow.versionId,
|
|
|
|
newVersionId: workflow.versionId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2022-12-06 00:25:39 -08:00
|
|
|
|
2022-10-26 06:49:43 -07:00
|
|
|
// check credentials for old format
|
|
|
|
await WorkflowHelpers.replaceInvalidCredentials(workflow);
|
|
|
|
|
|
|
|
WorkflowHelpers.addNodeIds(workflow);
|
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
await Container.get(ExternalHooks).run('workflow.update', [workflow]);
|
2022-10-26 06:49:43 -07:00
|
|
|
|
2023-11-17 06:58:50 -08:00
|
|
|
/**
|
|
|
|
* If the workflow being updated is stored as `active`, remove it from
|
|
|
|
* active workflows in memory, and re-add it after the update.
|
|
|
|
*
|
|
|
|
* If a trigger or poller in the workflow was updated, the new value
|
|
|
|
* will take effect only on removing and re-adding.
|
|
|
|
*/
|
2022-10-26 06:49:43 -07:00
|
|
|
if (shared.workflow.active) {
|
2023-02-21 10:21:56 -08:00
|
|
|
await Container.get(ActiveWorkflowRunner).remove(workflowId);
|
2022-10-26 06:49:43 -07:00
|
|
|
}
|
|
|
|
|
2023-03-24 05:11:48 -07:00
|
|
|
const workflowSettings = workflow.settings ?? {};
|
|
|
|
|
|
|
|
const keysAllowingDefault = [
|
|
|
|
'timezone',
|
|
|
|
'saveDataErrorExecution',
|
|
|
|
'saveDataSuccessExecution',
|
|
|
|
'saveManualExecutions',
|
|
|
|
'saveExecutionProgress',
|
|
|
|
] as const;
|
|
|
|
for (const key of keysAllowingDefault) {
|
|
|
|
// Do not save the default value
|
|
|
|
if (workflowSettings[key] === 'DEFAULT') {
|
|
|
|
delete workflowSettings[key];
|
2022-10-26 06:49:43 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 05:11:48 -07:00
|
|
|
if (workflowSettings.executionTimeout === config.get('executions.timeout')) {
|
|
|
|
// Do not save when default got set
|
|
|
|
delete workflowSettings.executionTimeout;
|
|
|
|
}
|
|
|
|
|
2022-10-26 06:49:43 -07:00
|
|
|
if (workflow.name) {
|
|
|
|
workflow.updatedAt = new Date(); // required due to atomic update
|
|
|
|
await validateEntity(workflow);
|
|
|
|
}
|
|
|
|
|
2023-11-10 06:04:26 -08:00
|
|
|
await Container.get(WorkflowRepository).update(
|
2022-11-21 06:51:23 -08:00
|
|
|
workflowId,
|
|
|
|
pick(workflow, [
|
|
|
|
'name',
|
|
|
|
'active',
|
|
|
|
'nodes',
|
|
|
|
'connections',
|
|
|
|
'settings',
|
|
|
|
'staticData',
|
|
|
|
'pinData',
|
2022-12-06 00:25:39 -08:00
|
|
|
'versionId',
|
2022-11-21 06:51:23 -08:00
|
|
|
]),
|
|
|
|
);
|
2022-10-26 06:49:43 -07:00
|
|
|
|
2023-03-30 07:25:51 -07:00
|
|
|
if (tagIds && !config.getEnv('workflowTagsDisabled')) {
|
2023-11-10 06:04:26 -08:00
|
|
|
await Container.get(WorkflowTagMappingRepository).delete({ workflowId });
|
|
|
|
await Container.get(WorkflowTagMappingRepository).insert(
|
2023-03-30 07:25:51 -07:00
|
|
|
tagIds.map((tagId) => ({ tagId, workflowId })),
|
|
|
|
);
|
2022-10-26 06:49:43 -07:00
|
|
|
}
|
|
|
|
|
2023-10-25 03:07:11 -07:00
|
|
|
if (!onlyActiveUpdate && workflow.versionId !== shared.workflow.versionId) {
|
2023-10-23 07:30:36 -07:00
|
|
|
await Container.get(WorkflowHistoryService).saveVersion(user, workflow, workflowId);
|
2023-09-27 07:22:39 -07:00
|
|
|
}
|
|
|
|
|
2023-01-02 08:42:32 -08:00
|
|
|
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
|
2022-10-26 06:49:43 -07:00
|
|
|
|
|
|
|
// We sadly get nothing back from "update". Neither if it updated a record
|
|
|
|
// nor the new value. So query now the hopefully updated entry.
|
2023-11-10 06:04:26 -08:00
|
|
|
const updatedWorkflow = await Container.get(WorkflowRepository).findOne({
|
2023-01-13 09:12:22 -08:00
|
|
|
where: { id: workflowId },
|
|
|
|
relations,
|
|
|
|
});
|
2022-10-26 06:49:43 -07:00
|
|
|
|
2023-01-13 09:12:22 -08:00
|
|
|
if (updatedWorkflow === null) {
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new BadRequestError(
|
2022-10-26 06:49:43 -07:00
|
|
|
`Workflow with ID "${workflowId}" could not be found to be updated.`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-30 07:25:51 -07:00
|
|
|
if (updatedWorkflow.tags?.length && tagIds?.length) {
|
2023-08-22 03:24:43 -07:00
|
|
|
updatedWorkflow.tags = Container.get(TagService).sortByRequestOrder(updatedWorkflow.tags, {
|
2023-03-30 07:25:51 -07:00
|
|
|
requestOrder: tagIds,
|
2022-10-26 06:49:43 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
await Container.get(ExternalHooks).run('workflow.afterUpdate', [updatedWorkflow]);
|
|
|
|
void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false);
|
2022-10-26 06:49:43 -07:00
|
|
|
|
|
|
|
if (updatedWorkflow.active) {
|
|
|
|
// When the workflow is supposed to be active add it again
|
|
|
|
try {
|
2023-02-21 10:21:56 -08:00
|
|
|
await Container.get(ExternalHooks).run('workflow.activate', [updatedWorkflow]);
|
|
|
|
await Container.get(ActiveWorkflowRunner).add(
|
2022-10-26 06:49:43 -07:00
|
|
|
workflowId,
|
|
|
|
shared.workflow.active ? 'update' : 'activate',
|
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
// If workflow could not be activated set it again to inactive
|
2023-02-20 03:22:27 -08:00
|
|
|
// and revert the versionId change so UI remains consistent
|
2023-11-10 06:04:26 -08:00
|
|
|
await Container.get(WorkflowRepository).update(workflowId, {
|
2023-02-20 03:22:27 -08:00
|
|
|
active: false,
|
|
|
|
versionId: shared.workflow.versionId,
|
|
|
|
});
|
2022-10-26 06:49:43 -07:00
|
|
|
|
|
|
|
// Also set it in the returned data
|
|
|
|
updatedWorkflow.active = false;
|
|
|
|
|
2023-02-01 16:00:24 -08:00
|
|
|
let message;
|
|
|
|
if (error instanceof NodeApiError) message = error.description;
|
|
|
|
message = message ?? (error as Error).message;
|
|
|
|
|
2022-10-26 06:49:43 -07:00
|
|
|
// Now return the original error for UI to display
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new BadRequestError(message);
|
2022-10-26 06:49:43 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-23 03:18:39 -08:00
|
|
|
const multiMainSetup = Container.get(MultiMainSetup);
|
2023-11-17 06:58:50 -08:00
|
|
|
|
2023-11-23 03:18:39 -08:00
|
|
|
await multiMainSetup.init();
|
2023-11-17 06:58:50 -08:00
|
|
|
|
2023-11-23 03:18:39 -08:00
|
|
|
if (multiMainSetup.isEnabled) {
|
|
|
|
await Container.get(MultiMainSetup).broadcastWorkflowActiveStateChanged({
|
|
|
|
workflowId,
|
|
|
|
oldState,
|
|
|
|
newState: updatedWorkflow.active,
|
|
|
|
versionId: shared.workflow.versionId,
|
|
|
|
});
|
2023-11-17 06:58:50 -08:00
|
|
|
}
|
|
|
|
|
2022-10-26 06:49:43 -07:00
|
|
|
return updatedWorkflow;
|
|
|
|
}
|
2022-11-11 02:14:45 -08:00
|
|
|
|
|
|
|
static async runManually(
|
|
|
|
{
|
|
|
|
workflowData,
|
|
|
|
runData,
|
|
|
|
pinData,
|
|
|
|
startNodes,
|
|
|
|
destinationNode,
|
|
|
|
}: WorkflowRequest.ManualRunPayload,
|
|
|
|
user: User,
|
|
|
|
sessionId?: string,
|
|
|
|
) {
|
|
|
|
const EXECUTION_MODE = 'manual';
|
|
|
|
const ACTIVATION_MODE = 'manual';
|
|
|
|
|
|
|
|
const pinnedTrigger = WorkflowsService.findPinnedTrigger(workflowData, startNodes, pinData);
|
|
|
|
|
|
|
|
// If webhooks nodes exist and are active we have to wait for till we receive a call
|
|
|
|
if (
|
|
|
|
pinnedTrigger === null &&
|
|
|
|
(runData === undefined ||
|
|
|
|
startNodes === undefined ||
|
|
|
|
startNodes.length === 0 ||
|
|
|
|
destinationNode === undefined)
|
|
|
|
) {
|
|
|
|
const workflow = new Workflow({
|
|
|
|
id: workflowData.id?.toString(),
|
|
|
|
name: workflowData.name,
|
|
|
|
nodes: workflowData.nodes,
|
|
|
|
connections: workflowData.connections,
|
|
|
|
active: false,
|
2023-02-21 10:21:56 -08:00
|
|
|
nodeTypes: Container.get(NodeTypes),
|
2022-11-11 02:14:45 -08:00
|
|
|
staticData: undefined,
|
|
|
|
settings: workflowData.settings,
|
|
|
|
});
|
|
|
|
|
|
|
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
|
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
const needsWebhook = await Container.get(TestWebhooks).needsWebhookData(
|
2022-11-11 02:14:45 -08:00
|
|
|
workflowData,
|
|
|
|
workflow,
|
|
|
|
additionalData,
|
|
|
|
EXECUTION_MODE,
|
|
|
|
ACTIVATION_MODE,
|
|
|
|
sessionId,
|
|
|
|
destinationNode,
|
|
|
|
);
|
|
|
|
if (needsWebhook) {
|
|
|
|
return {
|
|
|
|
waitingForWebhook: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// For manual testing always set to not active
|
|
|
|
workflowData.active = false;
|
|
|
|
|
|
|
|
// Start the workflow
|
|
|
|
const data: IWorkflowExecutionDataProcess = {
|
|
|
|
destinationNode,
|
|
|
|
executionMode: EXECUTION_MODE,
|
|
|
|
runData,
|
|
|
|
pinData,
|
|
|
|
sessionId,
|
|
|
|
startNodes,
|
|
|
|
workflowData,
|
|
|
|
userId: user.id,
|
|
|
|
};
|
|
|
|
|
|
|
|
const hasRunData = (node: INode) => runData !== undefined && !!runData[node.name];
|
|
|
|
|
|
|
|
if (pinnedTrigger && !hasRunData(pinnedTrigger)) {
|
|
|
|
data.startNodes = [pinnedTrigger.name];
|
|
|
|
}
|
|
|
|
|
|
|
|
const workflowRunner = new WorkflowRunner();
|
|
|
|
const executionId = await workflowRunner.run(data);
|
|
|
|
|
|
|
|
return {
|
|
|
|
executionId,
|
|
|
|
};
|
|
|
|
}
|
2023-01-10 00:23:44 -08:00
|
|
|
|
|
|
|
static async delete(user: User, workflowId: string): Promise<WorkflowEntity | undefined> {
|
2023-02-21 10:21:56 -08:00
|
|
|
await Container.get(ExternalHooks).run('workflow.delete', [workflowId]);
|
2023-01-10 00:23:44 -08:00
|
|
|
|
2023-11-10 06:04:26 -08:00
|
|
|
const sharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({
|
2023-01-10 00:23:44 -08:00
|
|
|
relations: ['workflow', 'role'],
|
|
|
|
where: whereClause({
|
|
|
|
user,
|
|
|
|
entityType: 'workflow',
|
|
|
|
entityId: workflowId,
|
|
|
|
roles: ['owner'],
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!sharedWorkflow) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sharedWorkflow.workflow.active) {
|
|
|
|
// deactivate before deleting
|
2023-02-21 10:21:56 -08:00
|
|
|
await Container.get(ActiveWorkflowRunner).remove(workflowId);
|
2023-01-10 00:23:44 -08:00
|
|
|
}
|
|
|
|
|
2023-11-10 06:04:26 -08:00
|
|
|
const idsForDeletion = await Container.get(ExecutionRepository)
|
|
|
|
.find({
|
|
|
|
select: ['id'],
|
|
|
|
where: { workflowId },
|
|
|
|
})
|
|
|
|
.then((rows) => rows.map(({ id: executionId }) => ({ workflowId, executionId })));
|
2023-10-10 01:06:06 -07:00
|
|
|
|
2023-11-10 06:04:26 -08:00
|
|
|
await Container.get(WorkflowRepository).delete(workflowId);
|
2023-10-10 01:06:06 -07:00
|
|
|
await Container.get(BinaryDataService).deleteMany(idsForDeletion);
|
2023-01-10 00:23:44 -08:00
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false);
|
|
|
|
await Container.get(ExternalHooks).run('workflow.afterDelete', [workflowId]);
|
2023-01-10 00:23:44 -08:00
|
|
|
|
|
|
|
return sharedWorkflow.workflow;
|
|
|
|
}
|
2023-02-02 08:01:45 -08:00
|
|
|
|
|
|
|
static async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise<UpdateResult> {
|
2023-11-10 06:04:26 -08:00
|
|
|
const qb = Container.get(WorkflowRepository).createQueryBuilder('workflow');
|
2023-02-02 08:01:45 -08:00
|
|
|
return qb
|
|
|
|
.update()
|
|
|
|
.set({
|
|
|
|
triggerCount,
|
|
|
|
updatedAt: () => {
|
|
|
|
if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) {
|
|
|
|
return 'updatedAt';
|
|
|
|
}
|
|
|
|
return '"updatedAt"';
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.where('id = :id', { id })
|
|
|
|
.execute();
|
|
|
|
}
|
2023-09-05 04:42:31 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Saves the static data if it changed
|
|
|
|
*/
|
|
|
|
static async saveStaticData(workflow: Workflow): Promise<void> {
|
|
|
|
if (workflow.staticData.__dataChanged === true) {
|
|
|
|
// Static data of workflow changed and so has to be saved
|
|
|
|
if (isWorkflowIdValid(workflow.id)) {
|
|
|
|
// Workflow is saved so update in database
|
|
|
|
try {
|
2023-09-25 09:04:52 -07:00
|
|
|
await WorkflowsService.saveStaticDataById(workflow.id, workflow.staticData);
|
2023-09-05 04:42:31 -07:00
|
|
|
workflow.staticData.__dataChanged = false;
|
|
|
|
} catch (error) {
|
|
|
|
ErrorReporter.error(error);
|
2023-10-25 07:35:22 -07:00
|
|
|
Container.get(Logger).error(
|
2023-09-05 04:42:31 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
|
|
`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${error.message}"`,
|
|
|
|
{ workflowId: workflow.id },
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Saves the given static data on workflow
|
|
|
|
*
|
|
|
|
* @param {(string)} workflowId The id of the workflow to save data on
|
|
|
|
* @param {IDataObject} newStaticData The static data to save
|
|
|
|
*/
|
|
|
|
static async saveStaticDataById(workflowId: string, newStaticData: IDataObject): Promise<void> {
|
2023-11-10 06:04:26 -08:00
|
|
|
await Container.get(WorkflowRepository).update(workflowId, {
|
2023-09-05 04:42:31 -07:00
|
|
|
staticData: newStaticData,
|
|
|
|
});
|
|
|
|
}
|
2022-10-11 05:55:05 -07:00
|
|
|
}
|