Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Prevent workflow history saving error from happening #7812

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export class WorkflowHistoryService {
}

async saveVersion(user: User, workflow: WorkflowEntity, workflowId: string) {
if (isWorkflowHistoryEnabled()) {
// On some update scenarios, `nodes` and `connections` are missing, such as when
// changing workflow settings or renaming. In these cases, we don't want to save
// a new version
if (isWorkflowHistoryEnabled() && workflow.nodes && workflow.connections) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for stuff like this we should really add different set of methods on the repository, and add strong typing on the payload that gets passed in. That way not only we wouldn't need vague checks with comments like these, but also the method names are going to be self explanatory. a good example is markAsCrashed in ExecutionRepository

try {
await this.workflowHistoryRepository.insert({
authors: user.firstName + ' ' + user.lastName,
Expand Down
21 changes: 5 additions & 16 deletions packages/cli/src/workflows/workflows.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NodeApiError, ErrorReporterProxy as ErrorReporter, Workflow } from 'n8n
import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere, UpdateResult } from 'typeorm';
import { In, Like } from 'typeorm';
import pick from 'lodash/pick';
import omit from 'lodash/omit';
import { v4 as uuid } from 'uuid';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as ResponseHelper from '@/ResponseHelper';
Expand Down Expand Up @@ -223,21 +224,9 @@ export class WorkflowsService {
);
}

let onlyActiveUpdate = false;

if (
(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)
) {
// we're just updating the active status of the workflow, don't update the versionId
onlyActiveUpdate = true;
} else {
// Update the workflow's version
if (Object.keys(omit(workflow, ['id', 'versionId', 'active'])).length > 0) {
// Update the workflow's version when changing properties such as
// `name`, `pinData`, `nodes`, `connections`, `settings` or `tags`
workflow.versionId = uuid();
logger.verbose(
`Updating versionId for workflow ${workflowId} for user ${user.id} after saving`,
Expand Down Expand Up @@ -308,7 +297,7 @@ export class WorkflowsService {
);
}

if (!onlyActiveUpdate && workflow.versionId !== shared.workflow.versionId) {
if (workflow.versionId !== shared.workflow.versionId) {
await Container.get(WorkflowHistoryService).saveVersion(user, workflow, workflowId);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/unit/WorkflowHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ function generateCredentialEntity(credentialId: string) {
return credentialEntity;
}

function getWorkflow(options?: {
export function getWorkflow(options?: {
addNodeWithoutCreds?: boolean;
addNodeWithOneCred?: boolean;
addNodeWithTwoCreds?: boolean;
Expand Down
33 changes: 1 addition & 32 deletions packages/cli/test/unit/services/ownership.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,9 @@ import { CacheService } from '@/services/cache.service';
import { User } from '@db/entities/User';
import { RoleService } from '@/services/role.service';
import { UserService } from '@/services/user.service';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import { mockInstance } from '../../shared/mocking';
import {
randomCredentialPayload,
randomEmail,
randomInteger,
randomName,
} from '../../integration/shared/random';

const wfOwnerRole = () =>
Object.assign(new Role(), {
scope: 'workflow',
name: 'owner',
id: randomInteger(),
});

const mockCredRole = (name: 'owner' | 'editor'): Role =>
Object.assign(new Role(), {
scope: 'credentials',
name,
id: randomInteger(),
});

const mockCredential = (): CredentialsEntity =>
Object.assign(new CredentialsEntity(), randomCredentialPayload());

const mockUser = (): User =>
Object.assign(new User(), {
id: randomInteger(),
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
});
import { mockCredRole, mockCredential, mockUser, wfOwnerRole } from '../shared/mockObjects';

describe('OwnershipService', () => {
const cacheService = mockInstance(CacheService);
Expand Down
114 changes: 114 additions & 0 deletions packages/cli/test/unit/services/workflowHistory.service.ee.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { User } from '@db/entities/User';
import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee';
import { mockInstance } from '../../shared/mocking';
import { Logger } from '@/Logger';
import { getWorkflow } from '../WorkflowHelpers.test';
import { mockClear } from 'jest-mock-extended';

const workflowHistoryRepository = mockInstance(WorkflowHistoryRepository);
const logger = mockInstance(Logger);
const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository);
const workflowHistoryService = new WorkflowHistoryService(
logger,
workflowHistoryRepository,
sharedWorkflowRepository,
);
const testUser = Object.assign(new User(), {
id: '1234',
password: 'passwordHash',
mfaEnabled: false,
firstName: 'John',
lastName: 'Doe',
});
let isWorkflowHistoryEnabled = true;

jest.mock('@/workflows/workflowHistory/workflowHistoryHelper.ee', () => {
return {
isWorkflowHistoryEnabled: jest.fn(() => isWorkflowHistoryEnabled),
};
});

describe('WorkflowHistoryService', () => {
beforeEach(() => {
mockClear(workflowHistoryRepository.insert);
});

describe('saveVersion', () => {
it('should save a new version when workflow history is enabled and nodes and connections are present', async () => {
// Arrange
isWorkflowHistoryEnabled = true;
const workflow = getWorkflow({ addNodeWithoutCreds: true });
const workflowId = '123';
workflow.connections = {};
workflow.id = workflowId;
workflow.versionId = '456';

// Act
await workflowHistoryService.saveVersion(testUser, workflow, workflowId);

// Assert
expect(workflowHistoryRepository.insert).toHaveBeenCalledWith({
authors: 'John Doe',
connections: {},
nodes: workflow.nodes,
versionId: workflow.versionId,
workflowId,
});
});

it('should not save a new version when workflow history is disabled', async () => {
// Arrange
isWorkflowHistoryEnabled = false;
const workflow = getWorkflow({ addNodeWithoutCreds: true });
const workflowId = '123';
workflow.connections = {};
workflow.id = workflowId;
workflow.versionId = '456';

// Act
await workflowHistoryService.saveVersion(testUser, workflow, workflowId);

// Assert
expect(workflowHistoryRepository.insert).not.toHaveBeenCalled();
});

it('should not save a new version when nodes or connections are missing', async () => {
// Arrange
isWorkflowHistoryEnabled = true;
const workflow = getWorkflow({ addNodeWithoutCreds: true });
const workflowId = '123';
workflow.id = workflowId;
workflow.versionId = '456';
// Nodes are set but connections is empty

// Act
await workflowHistoryService.saveVersion(testUser, workflow, workflowId);

// Assert
expect(workflowHistoryRepository.insert).not.toHaveBeenCalled();
});

it('should log an error when failed to save workflow history version', async () => {
// Arrange
isWorkflowHistoryEnabled = true;
const workflow = getWorkflow({ addNodeWithoutCreds: true });
const workflowId = '123';
workflow.connections = {};
workflow.id = workflowId;
workflow.versionId = '456';
workflowHistoryRepository.insert.mockRejectedValueOnce(new Error('Test error'));

// Act
await workflowHistoryService.saveVersion(testUser, workflow, workflowId);

// Assert
expect(workflowHistoryRepository.insert).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith(
'Failed to save workflow history version for workflow 123',
expect.any(Error),
);
});
});
});
35 changes: 35 additions & 0 deletions packages/cli/test/unit/shared/mockObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { User } from '@db/entities/User';
import { Role } from '@db/entities/Role';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';

import {
randomCredentialPayload,
randomEmail,
randomInteger,
randomName,
} from '../../integration/shared/random';

export const wfOwnerRole = () =>
Object.assign(new Role(), {
scope: 'workflow',
name: 'owner',
id: randomInteger(),
});

export const mockCredRole = (name: 'owner' | 'editor'): Role =>
Object.assign(new Role(), {
scope: 'credentials',
name,
id: randomInteger(),
});

export const mockCredential = (): CredentialsEntity =>
Object.assign(new CredentialsEntity(), randomCredentialPayload());

export const mockUser = (): User =>
Object.assign(new User(), {
id: randomInteger(),
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
});