Skip to content

Commit

Permalink
feat(core): Update PATCH /projects/:projectId/folders/:folderId to …
Browse files Browse the repository at this point in the history
…support tags (no-changelog) (#13456)
  • Loading branch information
RicardoE105 authored Feb 26, 2025
1 parent 3aa679e commit 27852e3
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -17,25 +29,34 @@ describe('UpdateFolderDto', () => {

describe('Invalid requests', () => {
test.each([
{
name: 'missing name',
request: {},
expectedErrorPath: ['name'],
},
{
name: 'empty name',
request: {
name: '',
},
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]);
}
});
});
Expand Down
5 changes: 3 additions & 2 deletions packages/@n8n/api-types/src/dto/folders/update-folder.dto.ts
Original file line number Diff line number Diff line change
@@ -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(),
}) {}
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
15 changes: 12 additions & 3 deletions packages/cli/src/services/folder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -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) {
Expand Down
54 changes: 54 additions & 0 deletions packages/cli/test/integration/folder/folder.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});

0 comments on commit 27852e3

Please sign in to comment.