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', () => {
|
describe('Valid requests', () => {
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
name: 'name without parentId',
|
name: 'name',
|
||||||
request: {
|
request: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'tagIds',
|
||||||
|
request: {
|
||||||
|
tagIds: ['1', '2'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'empty tagIds',
|
||||||
|
request: {
|
||||||
|
tagIds: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
])('should validate $name', ({ request }) => {
|
])('should validate $name', ({ request }) => {
|
||||||
const result = UpdateFolderDto.safeParse(request);
|
const result = UpdateFolderDto.safeParse(request);
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
@ -17,11 +29,6 @@ describe('UpdateFolderDto', () => {
|
||||||
|
|
||||||
describe('Invalid requests', () => {
|
describe('Invalid requests', () => {
|
||||||
test.each([
|
test.each([
|
||||||
{
|
|
||||||
name: 'missing name',
|
|
||||||
request: {},
|
|
||||||
expectedErrorPath: ['name'],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'empty name',
|
name: 'empty name',
|
||||||
request: {
|
request: {
|
||||||
|
@ -29,13 +36,27 @@ describe('UpdateFolderDto', () => {
|
||||||
},
|
},
|
||||||
expectedErrorPath: ['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 }) => {
|
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||||
const result = UpdateFolderDto.safeParse(request);
|
const result = UpdateFolderDto.safeParse(request);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|
||||||
if (expectedErrorPath) {
|
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 { Z } from 'zod-class';
|
||||||
|
|
||||||
import { folderNameSchema } from '../../schemas/folder.schema';
|
import { folderNameSchema } from '../../schemas/folder.schema';
|
||||||
|
|
||||||
export class UpdateFolderDto extends Z.class({
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import type { EntityManager } from '@n8n/typeorm';
|
import type { EntityManager } from '@n8n/typeorm';
|
||||||
|
|
||||||
|
import { FolderTagMappingRepository } from '@/databases/repositories/folder-tag-mapping.repository';
|
||||||
import { FolderRepository } from '@/databases/repositories/folder.repository';
|
import { FolderRepository } from '@/databases/repositories/folder.repository';
|
||||||
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||||
|
|
||||||
|
@ -20,7 +21,10 @@ interface FolderPathRow {
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class FolderService {
|
export class FolderService {
|
||||||
constructor(private readonly folderRepository: FolderRepository) {}
|
constructor(
|
||||||
|
private readonly folderRepository: FolderRepository,
|
||||||
|
private readonly folderTagMappingRepository: FolderTagMappingRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
async createFolder({ parentFolderId, name }: CreateFolderDto, projectId: string) {
|
async createFolder({ parentFolderId, name }: CreateFolderDto, projectId: string) {
|
||||||
let parentFolder = null;
|
let parentFolder = null;
|
||||||
|
@ -39,9 +43,14 @@ export class FolderService {
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFolder(folderId: string, projectId: string, { name }: UpdateFolderDto) {
|
async updateFolder(folderId: string, projectId: string, { name, tagIds }: UpdateFolderDto) {
|
||||||
await this.getFolderInProject(folderId, projectId);
|
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) {
|
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 { FolderRepository } from '@/databases/repositories/folder.repository';
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
import { createFolder } from '@test-integration/db/folders';
|
import { createFolder } from '@test-integration/db/folders';
|
||||||
|
import { createTag } from '@test-integration/db/tags';
|
||||||
|
|
||||||
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
||||||
import { createOwner, createMember } from '../shared/db/users';
|
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 });
|
const folderInDb = await folderRepository.findOneBy({ id: folder.id });
|
||||||
expect(folderInDb?.name).toBe('Updated Folder Name');
|
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