feat(core): Add endpoint DELETE /projects/:projectId/folders/:folderId (no-changelog) (#13516)

This commit is contained in:
Ricardo Espinoza 2025-02-27 16:47:07 -05:00 committed by GitHub
parent 6c266acced
commit 0c266b3060
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 429 additions and 19 deletions

View file

@ -1,9 +1,8 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { folderNameSchema } from '../../schemas/folder.schema';
import { folderNameSchema, folderId } from '../../schemas/folder.schema';
export class CreateFolderDto extends Z.class({
name: folderNameSchema,
parentFolderId: z.string().optional(),
parentFolderId: folderId.optional(),
}) {}

View file

@ -0,0 +1,7 @@
import { Z } from 'zod-class';
import { folderId } from '../../schemas/folder.schema';
export class DeleteFolderDto extends Z.class({
transferToFolderId: folderId.optional(),
}) {}

View file

@ -57,3 +57,4 @@ export { CreateApiKeyRequestDto } from './api-keys/create-api-key-request.dto';
export { CreateFolderDto } from './folders/create-folder.dto';
export { UpdateFolderDto } from './folders/update-folder.dto';
export { DeleteFolderDto } from './folders/delete-folder.dto';

View file

@ -1,3 +1,4 @@
import { z } from 'zod';
export const folderNameSchema = z.string().trim().min(1).max(128);
export const folderId = z.string().max(36);

View file

@ -22,5 +22,5 @@ export const RESOURCES = {
variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
folder: ['create', 'read', 'update'] as const,
folder: ['create', 'read', 'update', 'delete'] as const,
} as const;

View file

@ -1,7 +1,7 @@
import { CreateFolderDto, UpdateFolderDto } from '@n8n/api-types';
import { CreateFolderDto, DeleteFolderDto, UpdateFolderDto } from '@n8n/api-types';
import { Response } from 'express';
import { Post, RestController, ProjectScope, Body, Get, Patch } from '@/decorators';
import { Post, RestController, ProjectScope, Body, Get, Patch, Delete } from '@/decorators';
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
@ -26,7 +26,7 @@ export class ProjectController {
if (e instanceof FolderNotFoundError) {
throw new NotFoundError(e.message);
}
throw new InternalServerError();
throw new InternalServerError(undefined, e);
}
}
@ -45,7 +45,7 @@ export class ProjectController {
if (e instanceof FolderNotFoundError) {
throw new NotFoundError(e.message);
}
throw new InternalServerError();
throw new InternalServerError(undefined, e);
}
}
@ -64,7 +64,26 @@ export class ProjectController {
if (e instanceof FolderNotFoundError) {
throw new NotFoundError(e.message);
}
throw new InternalServerError();
throw new InternalServerError(undefined, e);
}
}
@Delete('/:folderId')
@ProjectScope('folder:delete')
async deleteFolder(
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
_res: Response,
@Body payload: DeleteFolderDto,
) {
const { projectId, folderId } = req.params;
try {
await this.folderService.deleteFolder(folderId, projectId, payload);
} catch (e) {
if (e instanceof FolderNotFoundError) {
throw new NotFoundError(e.message);
}
throw new InternalServerError(undefined, e);
}
}
}

View file

@ -22,7 +22,7 @@ export class Folder extends WithTimestampsAndStringId {
@Column()
name: string;
@ManyToOne(() => Folder, { nullable: true })
@ManyToOne(() => Folder, { nullable: true, onDelete: 'CASCADE' })
@JoinColumn({ name: 'parentFolderId' })
parentFolder: Folder | null;

View file

@ -100,7 +100,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
@ManyToOne('Folder', 'workflows', {
nullable: true,
onDelete: 'SET NULL',
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'parentFolderId' })
parentFolder: Folder | null;

View file

@ -0,0 +1,14 @@
import type { BaseMigration, MigrationContext } from '@/databases/types';
export class UpdateParentFolderIdColumn1740445074052 implements BaseMigration {
async up({ escape, queryRunner }: MigrationContext) {
const workflowTableName = escape.tableName('workflow_entity');
const folderTableName = escape.tableName('folder');
const parentFolderIdColumn = escape.columnName('parentFolderId');
const idColumn = escape.columnName('id');
await queryRunner.query(
`ALTER TABLE ${workflowTableName} ADD CONSTRAINT fk_workflow_parent_folder FOREIGN KEY (${parentFolderIdColumn}) REFERENCES ${folderTableName}(${idColumn}) ON DELETE CASCADE`,
);
}
}

View file

@ -82,6 +82,7 @@ import { CreateTestCaseExecutionTable1736947513045 } from '../common/17369475130
import { AddErrorColumnsToTestRuns1737715421462 } from '../common/1737715421462-AddErrorColumnsToTestRuns';
import { CreateFolderTable1738709609940 } from '../common/1738709609940-CreateFolderTable';
import { CreateAnalyticsTables1739549398681 } from '../common/1739549398681-CreateAnalyticsTables';
import { UpdateParentFolderIdColumn1740445074052 } from '../mysqldb/1740445074052-UpdateParentFolderIdColumn';
export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238,
@ -166,4 +167,5 @@ export const mysqlMigrations: Migration[] = [
CreateFolderTable1738709609940,
FixTestDefinitionPrimaryKey1739873751194,
CreateAnalyticsTables1739549398681,
UpdateParentFolderIdColumn1740445074052,
];

View file

@ -0,0 +1,36 @@
import { UnexpectedError } from 'n8n-workflow';
import type { BaseMigration, MigrationContext } from '@/databases/types';
export class UpdateParentFolderIdColumn1740445074052 implements BaseMigration {
async up({
escape,
queryRunner,
schemaBuilder: { dropForeignKey },
tablePrefix,
}: MigrationContext) {
const workflowTableName = escape.tableName('workflow_entity');
const folderTableName = escape.tableName('folder');
const parentFolderIdColumn = escape.columnName('parentFolderId');
const idColumn = escape.columnName('id');
const workflowTable = await queryRunner.getTable(`${tablePrefix}workflow_entity`);
if (!workflowTable) throw new UnexpectedError('Table workflow_entity not found');
const foreignKey = workflowTable.foreignKeys.find(
(fk) =>
fk.columnNames.includes('parentFolderId') &&
fk.referencedTableName === `${tablePrefix}folder`,
);
if (!foreignKey)
throw new UnexpectedError('Foreign key in column parentFolderId was not found');
await dropForeignKey('workflow_entity', 'parentFolderId', ['folder', 'id'], foreignKey.name);
await queryRunner.query(
`ALTER TABLE ${workflowTableName} ADD CONSTRAINT fk_workflow_parent_folder FOREIGN KEY (${parentFolderIdColumn}) REFERENCES ${folderTableName}(${idColumn}) ON DELETE CASCADE`,
);
}
}

View file

@ -44,6 +44,7 @@ import { MigrateToTimestampTz1694091729095 } from './1694091729095-MigrateToTime
import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting';
import { FixExecutionMetadataSequence1721377157740 } from './1721377157740-FixExecutionMetadataSequence';
import { MigrateTestDefinitionKeyToString1731582748663 } from './1731582748663-MigrateTestDefinitionKeyToString';
import { UpdateParentFolderIdColumn1740445074052 } from './1740445074052-UpdateParentFolderIdColumn';
import { CreateLdapEntities1674509946020 } from '../common/1674509946020-CreateLdapEntities';
import { PurgeInvalidWorkflowConnections1675940580449 } from '../common/1675940580449-PurgeInvalidWorkflowConnections';
import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns';
@ -164,4 +165,5 @@ export const postgresMigrations: Migration[] = [
AddErrorColumnsToTestRuns1737715421462,
CreateFolderTable1738709609940,
CreateAnalyticsTables1739549398681,
UpdateParentFolderIdColumn1740445074052,
];

View file

@ -0,0 +1,61 @@
import type { MigrationContext } from '@/databases/types';
import type { UpdateParentFolderIdColumn1740445074052 as BaseMigration } from './1740445074052-UpdateParentFolderIdColumn';
export class UpdateParentFolderIdColumn1740445074052 implements BaseMigration {
transaction = false as const;
async up({
queryRunner,
copyTable,
schemaBuilder: { createTable, column },
tablePrefix,
}: MigrationContext) {
await createTable('temp_workflow_entity')
.withColumns(
column('id').varchar(36).primary.notNull,
column('name').varchar(128).notNull,
column('active').bool.notNull,
column('nodes').json,
column('connections').json,
column('settings').json,
column('staticData').json,
column('pinData').json,
column('versionId').varchar(36),
column('triggerCount').int.default(0),
column('meta').json,
column('parentFolderId').varchar(36).default(null),
)
.withForeignKey('parentFolderId', {
tableName: 'folder',
columnName: 'id',
onDelete: 'CASCADE',
})
.withIndexOn(['name'], false).withTimestamps;
const columns = [
'id',
'name',
'active',
'nodes',
'connections',
'settings',
'staticData',
'pinData',
'versionId',
'triggerCount',
'meta',
'parentFolderId',
'createdAt',
'updatedAt',
];
await copyTable(`${tablePrefix}workflow_entity`, 'temp_workflow_entity', columns);
await queryRunner.query(`DROP TABLE "${tablePrefix}workflow_entity";`);
await queryRunner.query(
`ALTER TABLE "temp_workflow_entity" RENAME TO "${tablePrefix}workflow_entity";`,
);
}
}

View file

@ -43,6 +43,7 @@ import { AddProjectIcons1729607673469 } from './1729607673469-AddProjectIcons';
import { AddDescriptionToTestDefinition1731404028106 } from './1731404028106-AddDescriptionToTestDefinition';
import { MigrateTestDefinitionKeyToString1731582748663 } from './1731582748663-MigrateTestDefinitionKeyToString';
import { CreateFolderTable1738709609940 } from './1738709609940-CreateFolderTable';
import { UpdateParentFolderIdColumn1740445074052 } from './1740445074052-UpdateParentFolderIdColumn';
import { UniqueWorkflowNames1620821879465 } from '../common/1620821879465-UniqueWorkflowNames';
import { UpdateWorkflowCredentials1630330987096 } from '../common/1630330987096-UpdateWorkflowCredentials';
import { AddNodeIds1658930531669 } from '../common/1658930531669-AddNodeIds';
@ -158,6 +159,7 @@ const sqliteMigrations: Migration[] = [
AddErrorColumnsToTestRuns1737715421462,
CreateFolderTable1738709609940,
CreateAnalyticsTables1739549398681,
UpdateParentFolderIdColumn1740445074052,
];
export { sqliteMigrations };

View file

@ -1,6 +1,7 @@
import { Service } from '@n8n/di';
import type { EntityManager, SelectQueryBuilder } from '@n8n/typeorm';
import { DataSource, Repository } from '@n8n/typeorm';
import { PROJECT_ROOT } from 'n8n-workflow';
import type { ListQuery } from '@/requests';
@ -152,7 +153,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
});
}
if (filter?.parentFolderId === '0') {
if (filter?.parentFolderId === PROJECT_ROOT) {
query.andWhere('folder.parentFolderId IS NULL');
} else if (filter?.parentFolderId) {
query.andWhere('folder.parentFolderId = :parentFolderId', {
@ -242,4 +243,23 @@ export class FolderRepository extends Repository<FolderWithWorkflowCount> {
},
});
}
async moveAllToFolder(
fromFolderId: string,
toFolderId: string,
tx: EntityManager,
): Promise<void> {
await tx.update(
Folder,
{ parentFolder: { id: fromFolderId } },
{
parentFolder:
toFolderId === PROJECT_ROOT
? null
: {
id: toFolderId,
},
},
);
}
}

View file

@ -8,7 +8,9 @@ import type {
FindOptionsSelect,
FindManyOptions,
FindOptionsRelations,
EntityManager,
} from '@n8n/typeorm';
import { PROJECT_ROOT } from 'n8n-workflow';
import type { ListQuery } from '@/requests';
import { isStringArray } from '@/utils';
@ -394,7 +396,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
qb: SelectQueryBuilder<WorkflowEntity>,
filter: ListQuery.Options['filter'],
): void {
if (filter?.parentFolderId === '0') {
if (filter?.parentFolderId === PROJECT_ROOT) {
qb.andWhere('workflow.parentFolderId IS NULL');
} else if (filter?.parentFolderId) {
qb.andWhere('workflow.parentFolderId = :parentFolderId', {
@ -611,4 +613,16 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
async findByActiveState(activeState: boolean) {
return await this.findBy({ active: activeState });
}
async moveAllToFolder(fromFolderId: string, toFolderId: string, tx: EntityManager) {
await tx.update(
WorkflowEntity,
{ parentFolder: { id: fromFolderId } },
{
parentFolder: {
id: toFolderId,
},
},
);
}
}

View file

@ -28,6 +28,7 @@ export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [
'folder:create',
'folder:read',
'folder:update',
'folder:delete',
];
export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
@ -51,6 +52,7 @@ export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
'folder:create',
'folder:read',
'folder:update',
'folder:delete',
];
export const PROJECT_EDITOR_SCOPES: Scope[] = [
@ -70,6 +72,7 @@ export const PROJECT_EDITOR_SCOPES: Scope[] = [
'folder:create',
'folder:read',
'folder:update',
'folder:delete',
];
export const PROJECT_VIEWER_SCOPES: Scope[] = [

View file

@ -1,10 +1,12 @@
import type { CreateFolderDto, UpdateFolderDto } from '@n8n/api-types';
import type { CreateFolderDto, DeleteFolderDto, UpdateFolderDto } from '@n8n/api-types';
import { Service } from '@n8n/di';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import type { EntityManager } from '@n8n/typeorm';
import { Folder } from '@/databases/entities/folder';
import { FolderTagMappingRepository } from '@/databases/repositories/folder-tag-mapping.repository';
import { FolderRepository } from '@/databases/repositories/folder.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
export interface SimpleFolderNode {
@ -24,12 +26,13 @@ export class FolderService {
constructor(
private readonly folderRepository: FolderRepository,
private readonly folderTagMappingRepository: FolderTagMappingRepository,
private readonly workflowRepository: WorkflowRepository,
) {}
async createFolder({ parentFolderId, name }: CreateFolderDto, projectId: string) {
let parentFolder = null;
if (parentFolderId) {
parentFolder = await this.getFolderInProject(parentFolderId, projectId);
parentFolder = await this.findFolderInProjectOrFail(parentFolderId, projectId);
}
const folderEntity = this.folderRepository.create({
@ -44,7 +47,7 @@ export class FolderService {
}
async updateFolder(folderId: string, projectId: string, { name, tagIds }: UpdateFolderDto) {
await this.getFolderInProject(folderId, projectId);
await this.findFolderInProjectOrFail(folderId, projectId);
if (name) {
await this.folderRepository.update({ id: folderId }, { name });
}
@ -53,7 +56,7 @@ export class FolderService {
}
}
async getFolderInProject(folderId: string, projectId: string, em?: EntityManager) {
async findFolderInProjectOrFail(folderId: string, projectId: string, em?: EntityManager) {
try {
return await this.folderRepository.findOneOrFailFolderInProject(folderId, projectId, em);
} catch {
@ -62,7 +65,7 @@ export class FolderService {
}
async getFolderTree(folderId: string, projectId: string): Promise<SimpleFolderNode[]> {
await this.getFolderInProject(folderId, projectId);
await this.findFolderInProjectOrFail(folderId, projectId);
const escapedParentFolderId = this.folderRepository
.createQueryBuilder()
@ -103,6 +106,24 @@ export class FolderService {
return this.transformFolderPathToTree(result);
}
async deleteFolder(folderId: string, projectId: string, { transferToFolderId }: DeleteFolderDto) {
await this.findFolderInProjectOrFail(folderId, projectId);
if (!transferToFolderId) {
await this.folderRepository.delete({ id: folderId });
return;
}
await this.findFolderInProjectOrFail(transferToFolderId, projectId);
return await this.folderRepository.manager.transaction(async (tx) => {
await this.folderRepository.moveAllToFolder(folderId, transferToFolderId, tx);
await this.workflowRepository.moveAllToFolder(folderId, transferToFolderId, tx);
await tx.delete(Folder, { id: folderId });
return;
});
}
private transformFolderPathToTree(flatPath: FolderPathRow[]): SimpleFolderNode[] {
if (!flatPath || flatPath.length === 0) {
return [];

View file

@ -159,7 +159,7 @@ export class WorkflowsController {
if (parentFolderId) {
try {
const parentFolder = await this.folderService.getFolderInProject(
const parentFolder = await this.folderService.findFolderInProjectOrFail(
parentFolderId,
project.id,
transactionManager,

View file

@ -3,8 +3,10 @@ import { Container } from '@n8n/di';
import type { User } from '@/databases/entities/user';
import { FolderRepository } from '@/databases/repositories/folder.repository';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { createFolder } from '@test-integration/db/folders';
import { createTag } from '@test-integration/db/tags';
import { createWorkflow } from '@test-integration/db/workflows';
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
import { createOwner, createMember } from '../shared/db/users';
@ -23,12 +25,14 @@ const testServer = utils.setupTestServer({
let projectRepository: ProjectRepository;
let folderRepository: FolderRepository;
let workflowRepository: WorkflowRepository;
beforeEach(async () => {
await testDb.truncate(['Folder', 'SharedWorkflow', 'Tag', 'Project', 'ProjectRelation']);
projectRepository = Container.get(ProjectRepository);
folderRepository = Container.get(FolderRepository);
workflowRepository = Container.get(WorkflowRepository);
owner = await createOwner();
member = await createMember();
@ -463,3 +467,205 @@ describe('PATCH /projects/:projectId/folders/:folderId', () => {
expect(folderWithTags?.tags[0].id).toBe(tag3.id);
});
});
describe('DELETE /projects/:projectId/folders/:folderId', () => {
test('should not delete folder when project does not exist', async () => {
await authOwnerAgent
.delete('/projects/non-existing-id/folders/some-folder-id')
.send({})
.expect(403);
});
test('should not delete folder when folder does not exist', async () => {
const project = await createTeamProject('test project', owner);
await authOwnerAgent
.delete(`/projects/${project.id}/folders/non-existing-folder`)
.send({})
.expect(404);
});
test('should not delete folder if user has project:viewer role in team project', async () => {
const project = await createTeamProject(undefined, owner);
const folder = await createFolder(project);
await linkUserToProject(member, project, 'project:viewer');
await authMemberAgent
.delete(`/projects/${project.id}/folders/${folder.id}`)
.send({})
.expect(403);
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
expect(folderInDb).toBeDefined();
});
test("should not allow deleting folder in another user's personal project", async () => {
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
const folder = await createFolder(ownerPersonalProject);
await authMemberAgent
.delete(`/projects/${ownerPersonalProject.id}/folders/${folder.id}`)
.send({})
.expect(403);
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
expect(folderInDb).toBeDefined();
});
test('should delete folder if user has project:editor role in team project', async () => {
const project = await createTeamProject(undefined, owner);
const folder = await createFolder(project);
await linkUserToProject(member, project, 'project:editor');
await authMemberAgent
.delete(`/projects/${project.id}/folders/${folder.id}`)
.send({})
.expect(200);
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
expect(folderInDb).toBeNull();
});
test('should delete folder if user has project:admin role in team project', async () => {
const project = await createTeamProject(undefined, owner);
const folder = await createFolder(project);
await authOwnerAgent
.delete(`/projects/${project.id}/folders/${folder.id}`)
.send({})
.expect(200);
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
expect(folderInDb).toBeNull();
});
test('should delete folder in personal project', async () => {
const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
const folder = await createFolder(personalProject);
await authOwnerAgent
.delete(`/projects/${personalProject.id}/folders/${folder.id}`)
.send({})
.expect(200);
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
expect(folderInDb).toBeNull();
});
test('should delete folder, all child folders, and contained workflows when no transfer folder is specified', async () => {
const project = await createTeamProject('test', owner);
const rootFolder = await createFolder(project, { name: 'Root' });
const childFolder = await createFolder(project, {
name: 'Child',
parentFolder: rootFolder,
});
// Create workflows in the folders
const workflow1 = await createWorkflow({ parentFolder: rootFolder }, owner);
const workflow2 = await createWorkflow({ parentFolder: childFolder }, owner);
await authOwnerAgent.delete(`/projects/${project.id}/folders/${rootFolder.id}`);
// Check folders
const rootFolderInDb = await folderRepository.findOneBy({ id: rootFolder.id });
const childFolderInDb = await folderRepository.findOneBy({ id: childFolder.id });
expect(rootFolderInDb).toBeNull();
expect(childFolderInDb).toBeNull();
// Check workflows
const workflow1InDb = await workflowRepository.findOneBy({ id: workflow1.id });
const workflow2InDb = await workflowRepository.findOneBy({ id: workflow2.id });
expect(workflow1InDb).toBeNull();
expect(workflow2InDb).toBeNull();
});
test('should transfer folder contents when transferToFolderId is specified', async () => {
const project = await createTeamProject('test', owner);
const sourceFolder = await createFolder(project, { name: 'Source' });
const targetFolder = await createFolder(project, { name: 'Target' });
const childFolder = await createFolder(project, {
name: 'Child',
parentFolder: sourceFolder,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder }, owner);
const workflow2 = await createWorkflow({ parentFolder: childFolder }, owner);
const payload = {
transferToFolderId: targetFolder.id,
};
await authOwnerAgent
.delete(`/projects/${project.id}/folders/${sourceFolder.id}`)
.send(payload)
.expect(200);
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder.id },
relations: ['parentFolder'],
});
const childFolderInDb = await folderRepository.findOne({
where: { id: childFolder.id },
relations: ['parentFolder'],
});
// Check folders
expect(sourceFolderInDb).toBeNull();
expect(childFolderInDb).toBeDefined();
expect(childFolderInDb?.parentFolder?.id).toBe(targetFolder.id);
// Check workflows
const workflow1InDb = await workflowRepository.findOne({
where: { id: workflow1.id },
relations: ['parentFolder'],
});
expect(workflow1InDb).toBeDefined();
expect(workflow1InDb?.parentFolder?.id).toBe(targetFolder.id);
const workflow2InDb = await workflowRepository.findOne({
where: { id: workflow2.id },
relations: ['parentFolder'],
});
expect(workflow2InDb).toBeDefined();
expect(workflow2InDb?.parentFolder?.id).toBe(childFolder.id);
});
test('should not transfer folder contents when transfer folder does not exist', async () => {
const project = await createTeamProject('test', owner);
const folder = await createFolder(project);
const payload = {
transferToFolderId: 'non-existing-folder',
};
await authOwnerAgent
.delete(`/projects/${project.id}/folders/${folder.id}`)
.send(payload)
.expect(404);
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
expect(folderInDb).toBeDefined();
});
test('should not transfer folder contents when transfer folder is in another project', async () => {
const project1 = await createTeamProject('Project 1', owner);
const project2 = await createTeamProject('Project 2', owner);
const sourceFolder = await createFolder(project1);
const targetFolder = await createFolder(project2);
const payload = {
transferToFolderId: targetFolder.id,
};
await authOwnerAgent
.delete(`/projects/${project1.id}/folders/${sourceFolder.id}`)
.send(payload)
.expect(404);
const folderInDb = await folderRepository.findOneBy({ id: sourceFolder.id });
expect(folderInDb).toBeDefined();
});
});

View file

@ -101,3 +101,5 @@ export const FREE_AI_CREDITS_ERROR_TYPE = 'free_ai_credits_request_error';
export const FREE_AI_CREDITS_USED_ALL_CREDITS_ERROR_CODE = 400;
export const FROM_AI_AUTO_GENERATED_MARKER = '/*n8n-auto-generated-fromAI-override*/';
export const PROJECT_ROOT = '0';