mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Update PATCH /projects/:projectId/folders/:folderId
to support tags (no-changelog) (#13456)
This commit is contained in:
parent
3aa679e4ac
commit
27852e35ed
|
@ -4,11 +4,23 @@ describe('UpdateFolderDto', () => {
|
|||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'name without parentId',
|
||||
name: 'name',
|
||||
request: {
|
||||
name: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tagIds',
|
||||
request: {
|
||||
tagIds: ['1', '2'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'empty tagIds',
|
||||
request: {
|
||||
tagIds: [],
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = UpdateFolderDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
|
@ -17,11 +29,6 @@ describe('UpdateFolderDto', () => {
|
|||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'missing name',
|
||||
request: {},
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'empty name',
|
||||
request: {
|
||||
|
@ -29,13 +36,27 @@ describe('UpdateFolderDto', () => {
|
|||
},
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'non string tagIds',
|
||||
request: {
|
||||
tagIds: [0],
|
||||
},
|
||||
expectedErrorPath: ['tagIds'],
|
||||
},
|
||||
{
|
||||
name: 'non array tagIds',
|
||||
request: {
|
||||
tagIds: 0,
|
||||
},
|
||||
expectedErrorPath: ['tagIds'],
|
||||
},
|
||||
])('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);
|
||||
expect(result.error?.issues[0].path[0]).toEqual(expectedErrorPath[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { folderNameSchema } from '../../schemas/folder.schema';
|
||||
|
||||
export class UpdateFolderDto extends Z.class({
|
||||
name: folderNameSchema,
|
||||
name: folderNameSchema.optional(),
|
||||
tagIds: z.array(z.string().max(24)).optional(),
|
||||
}) {}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Service } from '@n8n/di';
|
||||
import { DataSource, Repository } from '@n8n/typeorm';
|
||||
|
||||
import { FolderTagMapping } from '../entities/folder-tag-mapping';
|
||||
|
||||
@Service()
|
||||
export class FolderTagMappingRepository extends Repository<FolderTagMapping> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(FolderTagMapping, dataSource.manager);
|
||||
}
|
||||
|
||||
async overwriteTags(folderId: string, tagIds: string[]) {
|
||||
return await this.manager.transaction(async (tx) => {
|
||||
await tx.delete(FolderTagMapping, { folderId });
|
||||
|
||||
const tags = tagIds.map((tagId) => this.create({ folderId, tagId }));
|
||||
|
||||
return await tx.insert(FolderTagMapping, tags);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { Service } from '@n8n/di';
|
|||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
|
||||
import { FolderTagMappingRepository } from '@/databases/repositories/folder-tag-mapping.repository';
|
||||
import { FolderRepository } from '@/databases/repositories/folder.repository';
|
||||
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||
|
||||
|
@ -20,7 +21,10 @@ interface FolderPathRow {
|
|||
|
||||
@Service()
|
||||
export class FolderService {
|
||||
constructor(private readonly folderRepository: FolderRepository) {}
|
||||
constructor(
|
||||
private readonly folderRepository: FolderRepository,
|
||||
private readonly folderTagMappingRepository: FolderTagMappingRepository,
|
||||
) {}
|
||||
|
||||
async createFolder({ parentFolderId, name }: CreateFolderDto, projectId: string) {
|
||||
let parentFolder = null;
|
||||
|
@ -39,9 +43,14 @@ export class FolderService {
|
|||
return folder;
|
||||
}
|
||||
|
||||
async updateFolder(folderId: string, projectId: string, { name }: UpdateFolderDto) {
|
||||
async updateFolder(folderId: string, projectId: string, { name, tagIds }: UpdateFolderDto) {
|
||||
await this.getFolderInProject(folderId, projectId);
|
||||
return await this.folderRepository.update({ id: folderId }, { name });
|
||||
if (name) {
|
||||
await this.folderRepository.update({ id: folderId }, { name });
|
||||
}
|
||||
if (tagIds) {
|
||||
await this.folderTagMappingRepository.overwriteTags(folderId, tagIds);
|
||||
}
|
||||
}
|
||||
|
||||
async getFolderInProject(folderId: string, projectId: string, em?: EntityManager) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { User } from '@/databases/entities/user';
|
|||
import { FolderRepository } from '@/databases/repositories/folder.repository';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { createFolder } from '@test-integration/db/folders';
|
||||
import { createTag } from '@test-integration/db/tags';
|
||||
|
||||
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
||||
import { createOwner, createMember } from '../shared/db/users';
|
||||
|
@ -408,4 +409,57 @@ describe('PATCH /projects/:projectId/folders/:folderId', () => {
|
|||
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||
expect(folderInDb?.name).toBe('Updated Folder Name');
|
||||
});
|
||||
|
||||
test('should update folder tags', async () => {
|
||||
const project = await createTeamProject('test project', owner);
|
||||
const folder = await createFolder(project, { name: 'Test Folder' });
|
||||
const tag1 = await createTag({ name: 'Tag 1' });
|
||||
const tag2 = await createTag({ name: 'Tag 2' });
|
||||
|
||||
const payload = {
|
||||
tagIds: [tag1.id, tag2.id],
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const folderWithTags = await folderRepository.findOne({
|
||||
where: { id: folder.id },
|
||||
relations: ['tags'],
|
||||
});
|
||||
|
||||
expect(folderWithTags?.tags).toHaveLength(2);
|
||||
expect(folderWithTags?.tags.map((t) => t.id).sort()).toEqual([tag1.id, tag2.id].sort());
|
||||
});
|
||||
|
||||
test('should replace existing folder tags with new ones', async () => {
|
||||
const project = await createTeamProject(undefined, owner);
|
||||
const tag1 = await createTag({ name: 'Tag 1' });
|
||||
const tag2 = await createTag({ name: 'Tag 2' });
|
||||
const tag3 = await createTag({ name: 'Tag 3' });
|
||||
|
||||
const folder = await createFolder(project, {
|
||||
name: 'Test Folder',
|
||||
tags: [tag1, tag2],
|
||||
});
|
||||
|
||||
const payload = {
|
||||
tagIds: [tag3.id],
|
||||
};
|
||||
|
||||
await authOwnerAgent
|
||||
.patch(`/projects/${project.id}/folders/${folder.id}`)
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
const folderWithTags = await folderRepository.findOne({
|
||||
where: { id: folder.id },
|
||||
relations: ['tags'],
|
||||
});
|
||||
|
||||
expect(folderWithTags?.tags).toHaveLength(1);
|
||||
expect(folderWithTags?.tags[0].id).toBe(tag3.id);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue