fix: Save new version of the workflow instead of the previous (no-changelog) (#7428)

Github issue / Community forum post (link here to close automatically):
This commit is contained in:
Val 2023-10-23 15:30:36 +01:00 committed by GitHub
parent b6de910cbe
commit 41236b7e08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 443 additions and 10 deletions

View file

@ -2,6 +2,7 @@ import type express from 'express';
import { Container } from 'typedi';
import type { FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import config from '@/config';
@ -36,6 +37,7 @@ export = {
const workflow = req.body;
workflow.active = false;
workflow.versionId = uuid();
await replaceInvalidCredentials(workflow);
@ -45,6 +47,14 @@ export = {
const createdWorkflow = await createWorkflow(workflow, req.user, role);
if (isWorkflowHistoryLicensed()) {
await Container.get(WorkflowHistoryService).saveVersion(
req.user,
createdWorkflow,
createdWorkflow.id,
);
}
await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]);
void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, true);
@ -151,6 +161,7 @@ export = {
const updateData = new WorkflowEntity();
Object.assign(updateData, req.body);
updateData.id = id;
updateData.versionId = uuid();
const sharedWorkflow = await getSharedWorkflow(req.user, id);
@ -179,10 +190,6 @@ export = {
}
}
if (isWorkflowHistoryLicensed()) {
await Container.get(WorkflowHistoryService).saveVersion(req.user, sharedWorkflow.workflow);
}
if (sharedWorkflow.workflow.active) {
try {
await workflowRunner.add(sharedWorkflow.workflowId, 'update');
@ -195,6 +202,14 @@ export = {
const updatedWorkflow = await getWorkflowById(sharedWorkflow.workflowId);
if (isWorkflowHistoryLicensed() && updatedWorkflow) {
await Container.get(WorkflowHistoryService).saveVersion(
req.user,
updatedWorkflow,
sharedWorkflow.workflowId,
);
}
await Container.get(ExternalHooks).run('workflow.afterUpdate', [updateData]);
void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true);

View file

@ -64,14 +64,14 @@ export class WorkflowHistoryService {
return hist;
}
async saveVersion(user: User, workflow: WorkflowEntity) {
async saveVersion(user: User, workflow: WorkflowEntity, workflowId: string) {
if (isWorkflowHistoryEnabled()) {
await this.workflowHistoryRepository.insert({
authors: user.firstName + ' ' + user.lastName,
connections: workflow.connections,
nodes: workflow.nodes,
versionId: workflow.versionId,
workflowId: workflow.id,
workflowId,
});
}
}

View file

@ -26,6 +26,8 @@ import { RoleService } from '@/services/role.service';
import * as utils from '@/utils';
import { listQueryMiddleware } from '@/middlewares';
import { TagService } from '@/services/tag.service';
import { isWorkflowHistoryLicensed } from './workflowHistory/workflowHistoryHelper.ee';
import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee';
export const workflowsController = express.Router();
@ -99,6 +101,14 @@ workflowsController.post(
throw new ResponseHelper.InternalServerError('Failed to save workflow');
}
if (isWorkflowHistoryLicensed()) {
await Container.get(WorkflowHistoryService).saveVersion(
req.user,
savedWorkflow,
savedWorkflow.id,
);
}
if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) {
savedWorkflow.tags = Container.get(TagService).sortByRequestOrder(savedWorkflow.tags, {
requestOrder: tagIds,

View file

@ -301,8 +301,8 @@ export class WorkflowsService {
);
}
if (isWorkflowHistoryLicensed()) {
await Container.get(WorkflowHistoryService).saveVersion(user, shared.workflow);
if (isWorkflowHistoryLicensed() && workflow.versionId !== shared.workflow.versionId) {
await Container.get(WorkflowHistoryService).saveVersion(user, workflow, workflowId);
}
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];

View file

@ -10,6 +10,9 @@ import * as utils from '../shared/utils/';
import * as testDb from '../shared/testDb';
import type { INode } from 'n8n-workflow';
import { STARTING_NODES } from '@/constants';
import { License } from '@/License';
import { WorkflowHistoryRepository } from '@/databases/repositories';
import Container from 'typedi';
let workflowOwnerRole: Role;
let owner: User;
@ -20,6 +23,11 @@ let workflowRunner: ActiveWorkflowRunner;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
const licenseLike = utils.mockInstance(License, {
isWorkflowHistoryLicensed: jest.fn().mockReturnValue(false),
isWithinUsersLimit: jest.fn().mockReturnValue(true),
});
beforeAll(async () => {
const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await testDb.getAllRoles();
@ -40,10 +48,18 @@ beforeAll(async () => {
});
beforeEach(async () => {
await testDb.truncate(['SharedCredentials', 'SharedWorkflow', 'Tag', 'Workflow', 'Credentials']);
await testDb.truncate([
'SharedCredentials',
'SharedWorkflow',
'Tag',
'Workflow',
'Credentials',
WorkflowHistoryRepository,
]);
authOwnerAgent = testServer.publicApiAgentFor(owner);
authMemberAgent = testServer.publicApiAgentFor(member);
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
});
afterEach(async () => {
@ -678,6 +694,90 @@ describe('POST /workflows', () => {
expect(sharedWorkflow?.role).toEqual(workflowOwnerRole);
});
test('should create workflow history version when licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(true);
const payload = {
name: 'testing',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
],
connections: {},
staticData: null,
settings: {
saveExecutionProgress: true,
saveManualExecutions: true,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
};
const response = await authMemberAgent.post('/workflows').send(payload);
expect(response.statusCode).toBe(200);
const { id } = response.body;
expect(id).toBeDefined();
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(1);
const historyVersion = await Container.get(WorkflowHistoryRepository).findOne({
where: {
workflowId: id,
},
});
expect(historyVersion).not.toBeNull();
expect(historyVersion!.connections).toEqual(payload.connections);
expect(historyVersion!.nodes).toEqual(payload.nodes);
});
test('should not create workflow history version when not licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
const payload = {
name: 'testing',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
],
connections: {},
staticData: null,
settings: {
saveExecutionProgress: true,
saveManualExecutions: true,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
};
const response = await authMemberAgent.post('/workflows').send(payload);
expect(response.statusCode).toBe(200);
const { id } = response.body;
expect(id).toBeDefined();
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(0);
});
test('should not add a starting node if the payload has no starting nodes', async () => {
const response = await authMemberAgent.post('/workflows').send({
name: 'testing',
@ -834,6 +934,108 @@ describe('PUT /workflows/:id', () => {
);
});
test('should create workflow history version when licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(true);
const workflow = await testDb.createWorkflow({}, member);
const payload = {
name: 'name updated',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
{
id: 'uuid-1234',
parameters: {},
name: 'Cron',
type: 'n8n-nodes-base.cron',
typeVersion: 1,
position: [400, 300],
},
],
connections: {},
staticData: '{"id":1}',
settings: {
saveExecutionProgress: false,
saveManualExecutions: false,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
};
const response = await authMemberAgent.put(`/workflows/${workflow.id}`).send(payload);
const { id } = response.body;
expect(response.statusCode).toBe(200);
expect(id).toBe(workflow.id);
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(1);
const historyVersion = await Container.get(WorkflowHistoryRepository).findOne({
where: {
workflowId: id,
},
});
expect(historyVersion).not.toBeNull();
expect(historyVersion!.connections).toEqual(payload.connections);
expect(historyVersion!.nodes).toEqual(payload.nodes);
});
test('should not create workflow history when not licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
const workflow = await testDb.createWorkflow({}, member);
const payload = {
name: 'name updated',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
{
id: 'uuid-1234',
parameters: {},
name: 'Cron',
type: 'n8n-nodes-base.cron',
typeVersion: 1,
position: [400, 300],
},
],
connections: {},
staticData: '{"id":1}',
settings: {
saveExecutionProgress: false,
saveManualExecutions: false,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
};
const response = await authMemberAgent.put(`/workflows/${workflow.id}`).send(payload);
const { id } = response.body;
expect(response.statusCode).toBe(200);
expect(id).toBe(workflow.id);
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(0);
});
test('should update non-owned workflow if owner', async () => {
const workflow = await testDb.createWorkflow({}, member);

View file

@ -11,6 +11,8 @@ import { v4 as uuid } from 'uuid';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import type { ListQuery } from '@/requests';
import { License } from '@/License';
import { WorkflowHistoryRepository } from '@/databases/repositories';
let owner: User;
let authOwnerAgent: SuperAgentTest;
@ -20,13 +22,19 @@ const testServer = utils.setupTestServer({ endpointGroups: ['workflows'] });
const { objectContaining, arrayContaining, any } = expect;
const licenseLike = utils.mockInstance(License, {
isWorkflowHistoryLicensed: jest.fn().mockReturnValue(false),
isWithinUsersLimit: jest.fn().mockReturnValue(true),
});
beforeAll(async () => {
owner = await testDb.createOwner();
authOwnerAgent = testServer.authAgentFor(owner);
});
beforeEach(async () => {
await testDb.truncate(['Workflow', 'SharedWorkflow', 'Tag']);
await testDb.truncate(['Workflow', 'SharedWorkflow', 'Tag', WorkflowHistoryRepository]);
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
});
describe('POST /workflows', () => {
@ -46,6 +54,96 @@ describe('POST /workflows', () => {
const pinData = await testWithPinData(false);
expect(pinData).toBeNull();
});
test('should create workflow history version when licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(true);
const payload = {
name: 'testing',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
],
connections: {},
staticData: null,
settings: {
saveExecutionProgress: true,
saveManualExecutions: true,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
active: false,
};
const response = await authOwnerAgent.post('/workflows').send(payload);
expect(response.statusCode).toBe(200);
const {
data: { id },
} = response.body;
expect(id).toBeDefined();
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(1);
const historyVersion = await Container.get(WorkflowHistoryRepository).findOne({
where: {
workflowId: id,
},
});
expect(historyVersion).not.toBeNull();
expect(historyVersion!.connections).toEqual(payload.connections);
expect(historyVersion!.nodes).toEqual(payload.nodes);
});
test('should not create workflow history version when not licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
const payload = {
name: 'testing',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
],
connections: {},
staticData: null,
settings: {
saveExecutionProgress: true,
saveManualExecutions: true,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
active: false,
};
const response = await authOwnerAgent.post('/workflows').send(payload);
expect(response.statusCode).toBe(200);
const {
data: { id },
} = response.body;
expect(id).toBeDefined();
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(0);
});
});
describe('GET /workflows/:id', () => {
@ -318,3 +416,111 @@ describe('GET /workflows', () => {
});
});
});
describe('PATCH /workflows/:id', () => {
test('should create workflow history version when licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(true);
const workflow = await testDb.createWorkflow({}, owner);
const payload = {
name: 'name updated',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
{
id: 'uuid-1234',
parameters: {},
name: 'Cron',
type: 'n8n-nodes-base.cron',
typeVersion: 1,
position: [400, 300],
},
],
connections: {},
staticData: '{"id":1}',
settings: {
saveExecutionProgress: false,
saveManualExecutions: false,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
};
const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);
const {
data: { id },
} = response.body;
expect(response.statusCode).toBe(200);
expect(id).toBe(workflow.id);
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(1);
const historyVersion = await Container.get(WorkflowHistoryRepository).findOne({
where: {
workflowId: id,
},
});
expect(historyVersion).not.toBeNull();
expect(historyVersion!.connections).toEqual(payload.connections);
expect(historyVersion!.nodes).toEqual(payload.nodes);
});
test('should not create workflow history version when not licensed', async () => {
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
const workflow = await testDb.createWorkflow({}, owner);
const payload = {
name: 'name updated',
nodes: [
{
id: 'uuid-1234',
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
{
id: 'uuid-1234',
parameters: {},
name: 'Cron',
type: 'n8n-nodes-base.cron',
typeVersion: 1,
position: [400, 300],
},
],
connections: {},
staticData: '{"id":1}',
settings: {
saveExecutionProgress: false,
saveManualExecutions: false,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
executionTimeout: 3600,
timezone: 'America/New_York',
},
};
const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload);
const {
data: { id },
} = response.body;
expect(response.statusCode).toBe(200);
expect(id).toBe(workflow.id);
expect(
await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }),
).toBe(0);
});
});