fix(core): Fix folders file overwrite for admin on push (#20813)

This commit is contained in:
Guillaume Jacquart 2025-10-15 15:49:22 +02:00 committed by GitHub
parent fde49fec4f
commit 4a3e7d7aec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 3 deletions

View File

@ -1,5 +1,6 @@
import type { SourceControlledFile } from '@n8n/api-types';
import type {
Folder,
FolderRepository,
Project,
ProjectRepository,
@ -57,6 +58,7 @@ describe('SourceControlExportService', () => {
);
const fsWriteFile = jest.spyOn(fsp, 'writeFile');
const fsReadFile = jest.spyOn(fsp, 'readFile');
beforeEach(() => jest.clearAllMocks());
@ -273,6 +275,63 @@ describe('SourceControlExportService', () => {
expect(result.count).toBe(0);
expect(result.files).toHaveLength(0);
});
it('should not duplicate folders on push', async () => {
// Arrange
const newFolders = [
{
id: 'folder-id',
name: 'Folder Name',
parentFolderId: null,
homeProject: { id: 'project-id' },
createdAt: new Date(),
updatedAt: new Date(),
} as Folder,
];
folderRepository.find.mockResolvedValue(newFolders);
workflowRepository.find.mockResolvedValue([mock()]);
const existingFolders = [
{
id: 'folder-id',
name: 'Folder Name',
parentFolderId: null,
homeProjectId: 'project-id',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
fsReadFile.mockResolvedValue(
JSON.stringify({
folders: existingFolders,
}),
);
// Act
const result = await service.exportFoldersToWorkFolder(globalAdminContext);
// Assert
// new json file should contain only the new folders
expect(fsWriteFile).toHaveBeenCalledWith(
'/mock/n8n/git/folders.json',
JSON.stringify(
{
folders: newFolders.map((f) => ({
id: f.id,
name: f.name,
parentFolderId: f.parentFolderId,
homeProjectId: f.homeProject.id,
createdAt: f.createdAt.toISOString(),
updatedAt: f.updatedAt.toISOString(),
})),
},
null,
2,
),
);
expect(result.count).toBe(1);
expect(result.files).toHaveLength(1);
});
});
describe('exportVariablesToWorkFolder', () => {

View File

@ -20,7 +20,6 @@ import { UnexpectedError, type ICredentialDataDecryptedObject } from 'n8n-workfl
import { rm as fsRm, writeFile as fsWriteFile } from 'node:fs/promises';
import path from 'path';
import { VariablesService } from '../variables/variables.service.ee';
import {
SOURCE_CONTROL_CREDENTIAL_EXPORT_FOLDER,
SOURCE_CONTROL_GIT_FOLDER,
@ -40,14 +39,15 @@ import {
stringContainsExpression,
} from './source-control-helper.ee';
import { SourceControlScopedService } from './source-control-scoped.service';
import { VariablesService } from '../variables/variables.service.ee';
import type { ExportResult } from './types/export-result';
import type { ExportableCredential } from './types/exportable-credential';
import { ExportableProject } from './types/exportable-project';
import type { ExportableWorkflow } from './types/exportable-workflow';
import type { RemoteResourceOwner } from './types/resource-owner';
import type { SourceControlContext } from './types/source-control-context';
import { formatWorkflow } from '@/workflows/workflow.formatter';
import { ExportableProject } from './types/exportable-project';
@Service()
export class SourceControlExportService {
@ -269,7 +269,7 @@ export class SourceControlExportService {
// keep all folders that are not accessible by the current user
// if allowedProjects is undefined, all folders are accessible by the current user
const foldersToKeepUnchanged = context.hasAccessToAllProjects()
? existingFolders.folders
? []
: existingFolders.folders.filter((folder) => {
return !allowedProjects.some((project) => project.id === folder.homeProjectId);
});