mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
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:
parent
b6de910cbe
commit
41236b7e08
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue