feat(core): Update PATCH /projects/:projectId/folders/:folderId to support tags (no-changelog) (#13456)

This commit is contained in:
Ricardo Espinoza 2025-02-26 07:30:59 -05:00 committed by GitHub
parent 3aa679e4ac
commit 27852e35ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 12 deletions

View file

@ -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]);
}
});
});

View file

@ -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(),
}) {}

View file

@ -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);
});
}
}

View file

@ -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) {

View file

@ -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);
});
});