mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Add endpoint PATCH /projects/:projectId/folders/:folderId
(no-changelog) (#13454)
This commit is contained in:
parent
b50658cbc6
commit
f7f5f5e95c
|
@ -0,0 +1,42 @@
|
|||
import { UpdateFolderDto } from '../update-folder.dto';
|
||||
|
||||
describe('UpdateFolderDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'name without parentId',
|
||||
request: {
|
||||
name: 'test',
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = UpdateFolderDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'missing name',
|
||||
request: {},
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'empty name',
|
||||
request: {
|
||||
name: '',
|
||||
},
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = UpdateFolderDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { folderNameSchema } from '../../schemas/folder.schema';
|
||||
|
||||
export class CreateFolderDto extends Z.class({
|
||||
name: z.string().trim().min(1).max(128),
|
||||
name: folderNameSchema,
|
||||
parentFolderId: z.string().optional(),
|
||||
}) {}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Z } from 'zod-class';
|
||||
|
||||
import { folderNameSchema } from '../../schemas/folder.schema';
|
||||
|
||||
export class UpdateFolderDto extends Z.class({
|
||||
name: folderNameSchema,
|
||||
}) {}
|
|
@ -56,3 +56,4 @@ export { UpdateApiKeyRequestDto } from './api-keys/update-api-key-request.dto';
|
|||
export { CreateApiKeyRequestDto } from './api-keys/create-api-key-request.dto';
|
||||
|
||||
export { CreateFolderDto } from './folders/create-folder.dto';
|
||||
export { UpdateFolderDto } from './folders/update-folder.dto';
|
||||
|
|
3
packages/@n8n/api-types/src/schemas/folder.schema.ts
Normal file
3
packages/@n8n/api-types/src/schemas/folder.schema.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const folderNameSchema = z.string().trim().min(1).max(128);
|
|
@ -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'] as const,
|
||||
folder: ['create', 'read', 'update'] as const,
|
||||
} as const;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { CreateFolderDto } from '@n8n/api-types';
|
||||
import { CreateFolderDto, UpdateFolderDto } from '@n8n/api-types';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { Post, RestController, ProjectScope, Body, Get } from '@/decorators';
|
||||
import { Post, RestController, ProjectScope, Body, Get, Patch } 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';
|
||||
|
@ -48,4 +48,23 @@ export class ProjectController {
|
|||
throw new InternalServerError();
|
||||
}
|
||||
}
|
||||
|
||||
@Patch('/:folderId')
|
||||
@ProjectScope('folder:update')
|
||||
async updateFolder(
|
||||
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
||||
_res: Response,
|
||||
@Body payload: UpdateFolderDto,
|
||||
) {
|
||||
const { projectId, folderId } = req.params;
|
||||
|
||||
try {
|
||||
await this.folderService.updateFolder(folderId, projectId, payload);
|
||||
} catch (e) {
|
||||
if (e instanceof FolderNotFoundError) {
|
||||
throw new NotFoundError(e.message);
|
||||
}
|
||||
throw new InternalServerError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [
|
|||
'project:delete',
|
||||
'folder:create',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
];
|
||||
|
||||
export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
|
||||
|
@ -49,6 +50,7 @@ export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
|
|||
'project:read',
|
||||
'folder:create',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
];
|
||||
|
||||
export const PROJECT_EDITOR_SCOPES: Scope[] = [
|
||||
|
@ -67,6 +69,7 @@ export const PROJECT_EDITOR_SCOPES: Scope[] = [
|
|||
'project:read',
|
||||
'folder:create',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
];
|
||||
|
||||
export const PROJECT_VIEWER_SCOPES: Scope[] = [
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CreateFolderDto } from '@n8n/api-types';
|
||||
import type { CreateFolderDto, 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';
|
||||
|
@ -39,6 +39,11 @@ export class FolderService {
|
|||
return folder;
|
||||
}
|
||||
|
||||
async updateFolder(folderId: string, projectId: string, { name }: UpdateFolderDto) {
|
||||
await this.getFolderInProject(folderId, projectId);
|
||||
return await this.folderRepository.update({ id: folderId }, { name });
|
||||
}
|
||||
|
||||
async getFolderInProject(folderId: string, projectId: string, em?: EntityManager) {
|
||||
try {
|
||||
return await this.folderRepository.findOneOrFailFolderInProject(folderId, projectId, em);
|
||||
|
|
|
@ -279,3 +279,133 @@ describe('GET /projects/:projectId/folders/:folderId/tree', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /projects/:projectId/folders/:folderId', () => {
|
||||
test('should not update folder when project does not exist', async () => {
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch('/projects/non-existing-id/folders/some-folder-id')
|
||||
.send(payload)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
test('should not update folder when folder does not exist', async () => {
|
||||
const project = await createTeamProject('test project', owner);
|
||||
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch(`/projects/${project.id}/folders/non-existing-folder`)
|
||||
.send(payload)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('should not update folder when name is empty', async () => {
|
||||
const project = await createTeamProject(undefined, owner);
|
||||
const folder = await createFolder(project, { name: 'Original Name' });
|
||||
|
||||
const payload = {
|
||||
name: '',
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(400);
|
||||
|
||||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Original Name');
|
||||
});
|
||||
|
||||
test('should not update folder if user has project:viewer role in team project', async () => {
|
||||
const project = await createTeamProject(undefined, owner);
|
||||
const folder = await createFolder(project, { name: 'Original Name' });
|
||||
await linkUserToProject(member, project, 'project:viewer');
|
||||
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(403);
|
||||
|
||||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Original Name');
|
||||
});
|
||||
|
||||
test("should not allow updating folder in another user's personal project", async () => {
|
||||
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const folder = await createFolder(ownerPersonalProject, { name: 'Original Name' });
|
||||
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.patch(`/projects/${ownerPersonalProject.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(403);
|
||||
|
||||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Original Name');
|
||||
});
|
||||
|
||||
test('should update folder if user has project:editor role in team project', async () => {
|
||||
const project = await createTeamProject(undefined, owner);
|
||||
const folder = await createFolder(project, { name: 'Original Name' });
|
||||
await linkUserToProject(member, project, 'project:editor');
|
||||
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authMemberAgent
|
||||
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Updated Folder Name');
|
||||
});
|
||||
|
||||
test('should update folder if user has project:admin role in team project', async () => {
|
||||
const project = await createTeamProject(undefined, owner);
|
||||
const folder = await createFolder(project, { name: 'Original Name' });
|
||||
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Updated Folder Name');
|
||||
});
|
||||
|
||||
test('should update folder in personal project', async () => {
|
||||
const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const folder = await createFolder(personalProject, { name: 'Original Name' });
|
||||
|
||||
const payload = {
|
||||
name: 'Updated Folder Name',
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch(`/projects/${personalProject.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Updated Folder Name');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue