2022-11-08 08:52:42 -08:00
|
|
|
import { JsonObject, jsonParse, LoggerProxy } from 'n8n-workflow';
|
|
|
|
import { FindManyOptions, FindOneOptions, In, ObjectLiteral } from 'typeorm';
|
2022-10-26 06:49:43 -07:00
|
|
|
import {
|
|
|
|
ActiveWorkflowRunner,
|
|
|
|
Db,
|
|
|
|
InternalHooksManager,
|
|
|
|
ResponseHelper,
|
|
|
|
whereClause,
|
|
|
|
WorkflowHelpers,
|
|
|
|
} from '..';
|
|
|
|
import config from '../../config';
|
2022-10-11 05:55:05 -07:00
|
|
|
import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
|
|
|
|
import { User } from '../databases/entities/User';
|
2022-10-11 07:40:39 -07:00
|
|
|
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
|
2022-10-26 06:49:43 -07:00
|
|
|
import { validateEntity } from '../GenericHelpers';
|
|
|
|
import { externalHooks } from '../Server';
|
|
|
|
import * as TagHelpers from '../TagHelpers';
|
2022-11-08 08:52:42 -08:00
|
|
|
import { getSharedWorkflowIds } from '../WorkflowHelpers';
|
|
|
|
import { validate as jsonSchemaValidate } from 'jsonschema';
|
|
|
|
|
|
|
|
export interface IGetWorkflowsQueryFilter {
|
|
|
|
id?: number | string;
|
|
|
|
name?: string;
|
|
|
|
active?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
const schemaGetWorkflowsQueryFilter = {
|
|
|
|
$id: '/IGetWorkflowsQueryFilter',
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
id: { anyOf: [{ type: 'integer' }, { type: 'string' }] },
|
|
|
|
name: { type: 'string' },
|
|
|
|
active: { type: 'boolean' },
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const allowedWorkflowsQueryFilterFields = Object.keys(schemaGetWorkflowsQueryFilter.properties);
|
2022-10-11 05:55:05 -07:00
|
|
|
|
|
|
|
export class WorkflowsService {
|
|
|
|
static async getSharing(
|
|
|
|
user: User,
|
|
|
|
workflowId: number | string,
|
|
|
|
relations: string[] = ['workflow'],
|
|
|
|
{ allowGlobalOwner } = { allowGlobalOwner: true },
|
|
|
|
): Promise<SharedWorkflow | undefined> {
|
|
|
|
const options: FindOneOptions<SharedWorkflow> & { where: ObjectLiteral } = {
|
|
|
|
where: {
|
|
|
|
workflow: { id: workflowId },
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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') {
|
|
|
|
options.where.user = { id: user.id };
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relations?.length) {
|
|
|
|
options.relations = relations;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Db.collections.SharedWorkflow.findOne(options);
|
|
|
|
}
|
2022-10-11 07:40:39 -07:00
|
|
|
|
|
|
|
static async get(workflow: Partial<WorkflowEntity>, options?: { relations: string[] }) {
|
|
|
|
return Db.collections.Workflow.findOne(workflow, options);
|
|
|
|
}
|
2022-10-26 06:49:43 -07:00
|
|
|
|
2022-11-08 08:52:42 -08:00
|
|
|
static async getMany(user: User, rawFilter: string) {
|
|
|
|
const sharedWorkflowIds = await getSharedWorkflowIds(user);
|
|
|
|
if (sharedWorkflowIds.length === 0) {
|
|
|
|
// return early since without shared workflows there can be no hits
|
|
|
|
// (note: getSharedWorkflowIds() returns _all_ workflow ids for global owners)
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
let filter: IGetWorkflowsQueryFilter | undefined = undefined;
|
|
|
|
if (rawFilter) {
|
|
|
|
try {
|
|
|
|
const filterJson: JsonObject = jsonParse(rawFilter);
|
|
|
|
if (filterJson) {
|
|
|
|
Object.keys(filterJson).map((key) => {
|
|
|
|
if (!allowedWorkflowsQueryFilterFields.includes(key)) delete filterJson[key];
|
|
|
|
});
|
|
|
|
if (jsonSchemaValidate(filterJson, schemaGetWorkflowsQueryFilter).valid) {
|
|
|
|
filter = filterJson as IGetWorkflowsQueryFilter;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
LoggerProxy.error('Failed to parse filter', {
|
|
|
|
userId: user.id,
|
|
|
|
filter,
|
|
|
|
});
|
|
|
|
throw new ResponseHelper.ResponseError(
|
|
|
|
`Parameter "filter" contained invalid JSON string.`,
|
|
|
|
500,
|
|
|
|
500,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// safeguard against querying ids not shared with the user
|
|
|
|
if (filter?.id !== undefined) {
|
|
|
|
const workflowId = parseInt(filter.id.toString());
|
|
|
|
if (workflowId && !sharedWorkflowIds.includes(workflowId)) {
|
|
|
|
LoggerProxy.verbose(`User ${user.id} attempted to query non-shared workflow ${workflowId}`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const fields: Array<keyof WorkflowEntity> = ['id', 'name', 'active', 'createdAt', 'updatedAt'];
|
|
|
|
|
|
|
|
const query: FindManyOptions<WorkflowEntity> = {
|
|
|
|
select: config.get('enterprise.features.sharing') ? [...fields, 'nodes'] : fields,
|
|
|
|
relations: config.get('enterprise.features.sharing')
|
|
|
|
? ['tags', 'shared', 'shared.user', 'shared.role']
|
|
|
|
: ['tags'],
|
|
|
|
};
|
|
|
|
|
|
|
|
if (config.getEnv('workflowTagsDisabled')) {
|
|
|
|
delete query.relations;
|
|
|
|
}
|
|
|
|
|
|
|
|
const workflows = await Db.collections.Workflow.find(
|
|
|
|
Object.assign(query, {
|
|
|
|
where: {
|
|
|
|
id: In(sharedWorkflowIds),
|
|
|
|
...filter,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
return workflows.map((workflow) => {
|
|
|
|
const { id, ...rest } = workflow;
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: id.toString(),
|
|
|
|
...rest,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-10-26 06:49:43 -07:00
|
|
|
static async updateWorkflow(
|
|
|
|
user: User,
|
|
|
|
workflow: WorkflowEntity,
|
|
|
|
workflowId: string,
|
|
|
|
tags?: string[],
|
2022-11-08 06:28:21 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2022-10-31 02:35:24 -07:00
|
|
|
forceSave?: boolean,
|
2022-10-26 06:49:43 -07:00
|
|
|
): Promise<WorkflowEntity> {
|
|
|
|
const shared = await Db.collections.SharedWorkflow.findOne({
|
|
|
|
relations: ['workflow'],
|
|
|
|
where: whereClause({
|
|
|
|
user,
|
|
|
|
entityType: 'workflow',
|
|
|
|
entityId: workflowId,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!shared) {
|
|
|
|
LoggerProxy.info('User attempted to update a workflow without permissions', {
|
|
|
|
workflowId,
|
|
|
|
userId: user.id,
|
|
|
|
});
|
|
|
|
throw new ResponseHelper.ResponseError(
|
|
|
|
`Workflow with ID "${workflowId}" could not be found to be updated.`,
|
|
|
|
undefined,
|
|
|
|
404,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-07 08:33:35 -08:00
|
|
|
// if (!forceSave && workflow.hash !== shared.workflow.hash) {
|
|
|
|
// throw new ResponseHelper.ResponseError(
|
|
|
|
// `Workflow ID ${workflowId} cannot be saved because it was changed by another user.`,
|
|
|
|
// undefined,
|
|
|
|
// 400,
|
|
|
|
// );
|
|
|
|
// }
|
2022-10-31 02:35:24 -07:00
|
|
|
|
2022-10-26 06:49:43 -07:00
|
|
|
// check credentials for old format
|
|
|
|
await WorkflowHelpers.replaceInvalidCredentials(workflow);
|
|
|
|
|
|
|
|
WorkflowHelpers.addNodeIds(workflow);
|
|
|
|
|
|
|
|
await externalHooks.run('workflow.update', [workflow]);
|
|
|
|
|
|
|
|
if (shared.workflow.active) {
|
|
|
|
// When workflow gets saved always remove it as the triggers could have been
|
|
|
|
// changed and so the changes would not take effect
|
|
|
|
await ActiveWorkflowRunner.getInstance().remove(workflowId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (workflow.settings) {
|
|
|
|
if (workflow.settings.timezone === 'DEFAULT') {
|
|
|
|
// Do not save the default timezone
|
|
|
|
delete workflow.settings.timezone;
|
|
|
|
}
|
|
|
|
if (workflow.settings.saveDataErrorExecution === 'DEFAULT') {
|
|
|
|
// Do not save when default got set
|
|
|
|
delete workflow.settings.saveDataErrorExecution;
|
|
|
|
}
|
|
|
|
if (workflow.settings.saveDataSuccessExecution === 'DEFAULT') {
|
|
|
|
// Do not save when default got set
|
|
|
|
delete workflow.settings.saveDataSuccessExecution;
|
|
|
|
}
|
|
|
|
if (workflow.settings.saveManualExecutions === 'DEFAULT') {
|
|
|
|
// Do not save when default got set
|
|
|
|
delete workflow.settings.saveManualExecutions;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
parseInt(workflow.settings.executionTimeout as string, 10) ===
|
|
|
|
config.get('executions.timeout')
|
|
|
|
) {
|
|
|
|
// Do not save when default got set
|
|
|
|
delete workflow.settings.executionTimeout;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (workflow.name) {
|
|
|
|
workflow.updatedAt = new Date(); // required due to atomic update
|
|
|
|
await validateEntity(workflow);
|
|
|
|
}
|
|
|
|
|
2022-10-31 02:35:24 -07:00
|
|
|
const { hash, ...rest } = workflow;
|
|
|
|
|
|
|
|
await Db.collections.Workflow.update(workflowId, rest);
|
2022-10-26 06:49:43 -07:00
|
|
|
|
|
|
|
if (tags && !config.getEnv('workflowTagsDisabled')) {
|
|
|
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
|
|
|
await TagHelpers.removeRelations(workflowId, tablePrefix);
|
|
|
|
|
|
|
|
if (tags.length) {
|
|
|
|
await TagHelpers.createRelations(workflowId, tags, tablePrefix);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const options: FindManyOptions<WorkflowEntity> = {
|
|
|
|
relations: ['tags'],
|
|
|
|
};
|
|
|
|
|
|
|
|
if (config.getEnv('workflowTagsDisabled')) {
|
|
|
|
delete options.relations;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We sadly get nothing back from "update". Neither if it updated a record
|
|
|
|
// nor the new value. So query now the hopefully updated entry.
|
|
|
|
const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, options);
|
|
|
|
|
|
|
|
if (updatedWorkflow === undefined) {
|
|
|
|
throw new ResponseHelper.ResponseError(
|
|
|
|
`Workflow with ID "${workflowId}" could not be found to be updated.`,
|
|
|
|
undefined,
|
|
|
|
400,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (updatedWorkflow.tags?.length && tags?.length) {
|
|
|
|
updatedWorkflow.tags = TagHelpers.sortByRequestOrder(updatedWorkflow.tags, {
|
|
|
|
requestOrder: tags,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
await externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
|
|
|
|
void InternalHooksManager.getInstance().onWorkflowSaved(user.id, updatedWorkflow, false);
|
|
|
|
|
|
|
|
if (updatedWorkflow.active) {
|
|
|
|
// When the workflow is supposed to be active add it again
|
|
|
|
try {
|
|
|
|
await externalHooks.run('workflow.activate', [updatedWorkflow]);
|
|
|
|
await ActiveWorkflowRunner.getInstance().add(
|
|
|
|
workflowId,
|
|
|
|
shared.workflow.active ? 'update' : 'activate',
|
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
// If workflow could not be activated set it again to inactive
|
|
|
|
workflow.active = false;
|
|
|
|
await Db.collections.Workflow.update(workflowId, workflow);
|
|
|
|
|
|
|
|
// Also set it in the returned data
|
|
|
|
updatedWorkflow.active = false;
|
|
|
|
|
|
|
|
// Now return the original error for UI to display
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return updatedWorkflow;
|
|
|
|
}
|
2022-10-11 05:55:05 -07:00
|
|
|
}
|