mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(core): Add method (getMany) to folder repository to filter, sort, and paginate folders (no-changelog) (#13290)
This commit is contained in:
21
packages/cli/src/databases/entities/folder-tag-mapping.ts
Normal file
21
packages/cli/src/databases/entities/folder-tag-mapping.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
||||
|
||||
import type { Folder } from './folder';
|
||||
import type { TagEntity } from './tag-entity';
|
||||
|
||||
@Entity({ name: 'folder_tag' })
|
||||
export class FolderTagMapping {
|
||||
@PrimaryColumn()
|
||||
folderId: string;
|
||||
|
||||
@ManyToOne('Folder', 'tagMappings')
|
||||
@JoinColumn({ name: 'folderId' })
|
||||
folders: Folder[];
|
||||
|
||||
@PrimaryColumn()
|
||||
tagId: string;
|
||||
|
||||
@ManyToOne('TagEntity', 'folderMappings')
|
||||
@JoinColumn({ name: 'tagId' })
|
||||
tags: TagEntity[];
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { ExecutionData } from './execution-data';
|
||||
import { ExecutionEntity } from './execution-entity';
|
||||
import { ExecutionMetadata } from './execution-metadata';
|
||||
import { Folder } from './folder';
|
||||
import { FolderTagMapping } from './folder-tag-mapping';
|
||||
import { InstalledNodes } from './installed-nodes';
|
||||
import { InstalledPackages } from './installed-packages';
|
||||
import { InvalidAuthToken } from './invalid-auth-token';
|
||||
@@ -68,4 +69,5 @@ export const entities = {
|
||||
TestRun,
|
||||
TestCaseExecution,
|
||||
Folder,
|
||||
FolderTagMapping,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Column, Entity, Index, ManyToMany, OneToMany } from '@n8n/typeorm';
|
||||
import { IsString, Length } from 'class-validator';
|
||||
|
||||
import { WithTimestampsAndStringId } from './abstract-entity';
|
||||
import type { FolderTagMapping } from './folder-tag-mapping';
|
||||
import type { WorkflowEntity } from './workflow-entity';
|
||||
import type { WorkflowTagMapping } from './workflow-tag-mapping';
|
||||
|
||||
@@ -18,4 +19,7 @@ export class TagEntity extends WithTimestampsAndStringId {
|
||||
|
||||
@OneToMany('WorkflowTagMapping', 'tags')
|
||||
workflowMappings: WorkflowTagMapping[];
|
||||
|
||||
@OneToMany('FolderTagMapping', 'tags')
|
||||
folderMappings: FolderTagMapping[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
import { Container } from '@n8n/di';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import type { Folder } from '@/databases/entities/folder';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||
import { createFolder } from '@test-integration/db/folders';
|
||||
import { getPersonalProject } from '@test-integration/db/projects';
|
||||
import { createTag } from '@test-integration/db/tags';
|
||||
import { createMember, createOwner } from '@test-integration/db/users';
|
||||
import { createWorkflow } from '@test-integration/db/workflows';
|
||||
|
||||
import * as testDb from '../../../../test/integration/shared/test-db';
|
||||
import { FolderRepository } from '../folder.repository';
|
||||
|
||||
describe('FolderRepository', () => {
|
||||
let folderRepository: FolderRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
folderRepository = Container.get(FolderRepository);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['Folder', 'Tag']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('getMany', () => {
|
||||
let project: Project;
|
||||
let owner: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createOwner();
|
||||
project = await getPersonalProject(owner);
|
||||
});
|
||||
|
||||
describe('filters', () => {
|
||||
it('should return all folders if not filter is provided', async () => {
|
||||
const folder1 = await createFolder(project, { name: 'folder1' });
|
||||
const folder2 = await createFolder(project, { name: 'folder2' });
|
||||
|
||||
await Promise.all([folder1, folder2]);
|
||||
|
||||
const [folders, count] = await folderRepository.getMany();
|
||||
expect(count).toBe(2);
|
||||
expect(folders).toHaveLength(2);
|
||||
|
||||
expect(folders[1].name).toBe('folder1');
|
||||
expect(folders[0].name).toBe('folder2');
|
||||
|
||||
folders.forEach((folder) => {
|
||||
expect(folder).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
parentFolder: null,
|
||||
project: {
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
type: expect.any(String),
|
||||
icon: null,
|
||||
},
|
||||
tags: expect.any(Array),
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should filter folders by project ID', async () => {
|
||||
const anotherUser = await createMember();
|
||||
const anotherProject = await getPersonalProject(anotherUser);
|
||||
|
||||
const folder1 = createFolder(project, { name: 'folder1' });
|
||||
const folder2 = createFolder(anotherProject, { name: 'folder2' });
|
||||
|
||||
await Promise.all([folder1, folder2]);
|
||||
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
filter: { projectId: project.id },
|
||||
});
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(folders).toHaveLength(1);
|
||||
expect(folders[0].name).toBe('folder1');
|
||||
expect(folders[0].project.id).toBe(project.id);
|
||||
});
|
||||
|
||||
it('should filter folders by name case-insensitively', async () => {
|
||||
const folder1 = createFolder(project, { name: 'Test Folder' });
|
||||
const folder2 = createFolder(project, { name: 'Another Folder' });
|
||||
const folder3 = createFolder(project, { name: 'test folder sub' });
|
||||
|
||||
await Promise.all([folder1, folder2, folder3]);
|
||||
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
filter: { name: 'test' },
|
||||
});
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(folders).toHaveLength(2);
|
||||
expect(folders.map((f) => f.name).sort()).toEqual(['Test Folder', 'test folder sub']);
|
||||
});
|
||||
|
||||
it('should filter folders by parent folder ID', async () => {
|
||||
const parentFolder = await createFolder(project, { name: 'Parent' });
|
||||
await createFolder(project, {
|
||||
name: 'Child 1',
|
||||
parentFolder,
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Child 2',
|
||||
parentFolder,
|
||||
});
|
||||
await createFolder(project, { name: 'Unrelated' });
|
||||
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
filter: { parentFolderId: parentFolder.id },
|
||||
});
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(folders).toHaveLength(2);
|
||||
expect(folders.map((f) => f.name).sort()).toEqual(['Child 1', 'Child 2']);
|
||||
folders.forEach((folder) => {
|
||||
expect(folder.parentFolder?.id).toBe(parentFolder.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter folders by a single tag', async () => {
|
||||
const tag1 = await createTag({ name: 'important' });
|
||||
const tag2 = await createTag({ name: 'archived' });
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'Folder 1',
|
||||
tags: [tag1],
|
||||
});
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'Folder 2',
|
||||
tags: [tag2],
|
||||
});
|
||||
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
filter: { tags: ['important'] },
|
||||
});
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(folders).toHaveLength(1);
|
||||
expect(folders[0].name).toBe('Folder 1');
|
||||
expect(folders[0].tags[0].name).toBe('important');
|
||||
});
|
||||
|
||||
it('should filter folders by multiple tags (AND operator)', async () => {
|
||||
const tag1 = await createTag({ name: 'important' });
|
||||
const tag2 = await createTag({ name: 'active' });
|
||||
const tag3 = await createTag({ name: 'archived' });
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'Folder 1',
|
||||
tags: [tag1, tag2],
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Folder 2',
|
||||
tags: [tag1],
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Folder 3',
|
||||
tags: [tag3],
|
||||
});
|
||||
|
||||
const [folders] = await folderRepository.getMany({
|
||||
filter: { tags: ['important', 'active'] },
|
||||
});
|
||||
|
||||
expect(folders).toHaveLength(1);
|
||||
expect(folders[0].name).toBe('Folder 1');
|
||||
});
|
||||
|
||||
it('should apply multiple filters together', async () => {
|
||||
const tag1 = await createTag({ name: 'important' });
|
||||
const tag2 = await createTag({ name: 'archived' });
|
||||
|
||||
const parentFolder = await createFolder(project, { name: 'Parent' });
|
||||
await createFolder(project, {
|
||||
name: 'Test Folder',
|
||||
parentFolder,
|
||||
tags: [tag1],
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Test Another',
|
||||
tags: [tag1],
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Test Child',
|
||||
parentFolder,
|
||||
tags: [tag2],
|
||||
});
|
||||
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
filter: {
|
||||
name: 'test',
|
||||
parentFolderId: parentFolder.id,
|
||||
tags: ['important'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(folders).toHaveLength(1);
|
||||
expect(folders[0].name).toBe('Test Folder');
|
||||
expect(folders[0].parentFolder?.id).toBe(parentFolder.id);
|
||||
expect(folders[0].tags[0].name).toBe('important');
|
||||
});
|
||||
});
|
||||
|
||||
describe('select', () => {
|
||||
let testFolder: Folder;
|
||||
let workflowWithTestFolder: WorkflowEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
const parentFolder = await createFolder(project, { name: 'Parent Folder' });
|
||||
const tag = await createTag({ name: 'test-tag' });
|
||||
testFolder = await createFolder(project, {
|
||||
name: 'Test Folder',
|
||||
parentFolder,
|
||||
tags: [tag],
|
||||
});
|
||||
workflowWithTestFolder = await createWorkflow({ parentFolder: testFolder });
|
||||
});
|
||||
|
||||
it('should select only id and name when specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(folders).toEqual([
|
||||
{
|
||||
id: expect.any(String),
|
||||
name: 'Test Folder',
|
||||
},
|
||||
{
|
||||
id: expect.any(String),
|
||||
name: 'Parent Folder',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id, name and tags when specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
tags: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(folders).toHaveLength(2);
|
||||
folders.forEach((folder) => {
|
||||
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'tags']);
|
||||
expect(folder.id).toBeDefined();
|
||||
expect(folder.name).toBeDefined();
|
||||
expect(Array.isArray(folder.tags)).toBeTruthy();
|
||||
});
|
||||
|
||||
const folderWithTag = folders.find((f) => f.name === 'Test Folder');
|
||||
expect(folderWithTag?.tags).toHaveLength(1);
|
||||
expect(folderWithTag?.tags[0]).toEqual({
|
||||
id: expect.any(String),
|
||||
name: 'test-tag',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id, name and project when specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(folders).toHaveLength(2);
|
||||
folders.forEach((folder) => {
|
||||
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'project']);
|
||||
expect(folder.project).toEqual({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
type: expect.any(String),
|
||||
icon: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id, name and parentFolder when specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
parentFolder: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(folders).toHaveLength(2);
|
||||
folders.forEach((folder) => {
|
||||
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'parentFolder']);
|
||||
});
|
||||
|
||||
const parentFolder = folders.find((f) => f.name === 'Parent Folder');
|
||||
expect(parentFolder?.parentFolder).toBeNull();
|
||||
|
||||
const childFolder = folders.find((f) => f.name === 'Test Folder');
|
||||
expect(childFolder?.parentFolder).toEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Parent Folder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id, name and workflows when specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
workflows: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(folders).toHaveLength(2);
|
||||
folders.forEach((folder) => {
|
||||
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'workflows']);
|
||||
expect(folder.id).toBeDefined();
|
||||
expect(folder.name).toBeDefined();
|
||||
expect(Array.isArray(folder.workflows)).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(folders[0].workflows).toHaveLength(1);
|
||||
expect(folders[0].workflows[0].id).toBe(workflowWithTestFolder.id);
|
||||
});
|
||||
|
||||
it('should return timestamps when specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(folders).toHaveLength(2);
|
||||
folders.forEach((folder) => {
|
||||
expect(Object.keys(folder).sort()).toEqual(['createdAt', 'id', 'updatedAt']);
|
||||
expect(folder.createdAt).toBeInstanceOf(Date);
|
||||
expect(folder.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all properties when no select is specified', async () => {
|
||||
const [folders] = await folderRepository.getMany();
|
||||
|
||||
expect(folders).toHaveLength(2);
|
||||
folders.forEach((folder) => {
|
||||
expect(folder).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
project: {
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
type: expect.any(String),
|
||||
icon: null,
|
||||
},
|
||||
workflows: expect.any(Array),
|
||||
tags: expect.any(Array),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
beforeEach(async () => {
|
||||
// Create 5 folders sequentially and ensure consistent updatedAt order
|
||||
await createFolder(project, {
|
||||
name: 'Folder 1',
|
||||
updatedAt: DateTime.now().minus({ minutes: 4 }).toJSDate(),
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Folder 2',
|
||||
updatedAt: DateTime.now().minus({ minutes: 3 }).toJSDate(),
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Folder 3',
|
||||
updatedAt: DateTime.now().minus({ minutes: 2 }).toJSDate(),
|
||||
});
|
||||
await createFolder(project, {
|
||||
name: 'Folder 4',
|
||||
updatedAt: DateTime.now().minus({ minutes: 1 }).toJSDate(),
|
||||
});
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'Folder 5',
|
||||
updatedAt: DateTime.now().toJSDate(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit results when take is specified', async () => {
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
take: 3,
|
||||
});
|
||||
|
||||
expect(count).toBe(5);
|
||||
expect(folders).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should skip results when skip is specified', async () => {
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
skip: 2,
|
||||
take: 5,
|
||||
});
|
||||
|
||||
expect(count).toBe(5);
|
||||
expect(folders).toHaveLength(3);
|
||||
expect(folders.map((f) => f.name)).toEqual(['Folder 3', 'Folder 2', 'Folder 1']);
|
||||
});
|
||||
|
||||
it('should handle skip and take together', async () => {
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
skip: 1,
|
||||
take: 2,
|
||||
});
|
||||
|
||||
expect(count).toBe(5);
|
||||
expect(folders).toHaveLength(2);
|
||||
expect(folders.map((f) => f.name)).toEqual(['Folder 4', 'Folder 3']);
|
||||
});
|
||||
|
||||
it('should handle take larger than remaining items', async () => {
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
skip: 3,
|
||||
take: 10,
|
||||
});
|
||||
|
||||
expect(count).toBe(5);
|
||||
expect(folders).toHaveLength(2);
|
||||
expect(folders.map((f) => f.name)).toEqual(['Folder 2', 'Folder 1']);
|
||||
});
|
||||
|
||||
it('should handle zero take by returning all results', async () => {
|
||||
const [folders, count] = await folderRepository.getMany({
|
||||
take: 0,
|
||||
});
|
||||
|
||||
expect(count).toBe(5);
|
||||
expect(folders).toHaveLength(5);
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'Folder 5',
|
||||
'Folder 4',
|
||||
'Folder 3',
|
||||
'Folder 2',
|
||||
'Folder 1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
beforeEach(async () => {
|
||||
// Create 4 folders sequentially and ensure consistent updatedAt order
|
||||
await createFolder(project, {
|
||||
name: 'B Folder',
|
||||
createdAt: DateTime.now().toJSDate(),
|
||||
updatedAt: DateTime.now().toJSDate(),
|
||||
});
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'A Folder',
|
||||
createdAt: DateTime.now().plus({ minutes: 1 }).toJSDate(),
|
||||
updatedAt: DateTime.now().plus({ minutes: 1 }).toJSDate(),
|
||||
});
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'D Folder',
|
||||
createdAt: DateTime.now().plus({ minutes: 2 }).toJSDate(),
|
||||
updatedAt: DateTime.now().plus({ minutes: 2 }).toJSDate(),
|
||||
});
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'C Folder',
|
||||
createdAt: DateTime.now().plus({ minutes: 3 }).toJSDate(),
|
||||
updatedAt: DateTime.now().plus({ minutes: 3 }).toJSDate(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort by default (updatedAt:desc)', async () => {
|
||||
const [folders] = await folderRepository.getMany();
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'C Folder',
|
||||
'D Folder',
|
||||
'A Folder',
|
||||
'B Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by name:asc', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'name:asc',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'A Folder',
|
||||
'B Folder',
|
||||
'C Folder',
|
||||
'D Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by name:desc', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'name:desc',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'D Folder',
|
||||
'C Folder',
|
||||
'B Folder',
|
||||
'A Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by createdAt:asc', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'createdAt:asc',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'B Folder',
|
||||
'A Folder',
|
||||
'D Folder',
|
||||
'C Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by createdAt:desc', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'createdAt:desc',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'C Folder',
|
||||
'D Folder',
|
||||
'A Folder',
|
||||
'B Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by updatedAt:asc', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'updatedAt:asc',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'B Folder',
|
||||
'A Folder',
|
||||
'D Folder',
|
||||
'C Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by updatedAt:desc', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'updatedAt:desc',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'C Folder',
|
||||
'D Folder',
|
||||
'A Folder',
|
||||
'B Folder',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should default to asc if order not specified', async () => {
|
||||
const [folders] = await folderRepository.getMany({
|
||||
sortBy: 'name',
|
||||
});
|
||||
|
||||
expect(folders.map((f) => f.name)).toEqual([
|
||||
'A Folder',
|
||||
'B Folder',
|
||||
'C Folder',
|
||||
'D Folder',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,200 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { DataSource, Repository } from '@n8n/typeorm';
|
||||
|
||||
import type { ListQuery } from '@/requests';
|
||||
|
||||
import { Folder } from '../entities/folder';
|
||||
import { FolderTagMapping } from '../entities/folder-tag-mapping';
|
||||
import { TagEntity } from '../entities/tag-entity';
|
||||
|
||||
@Service()
|
||||
export class FolderRepository extends Repository<Folder> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(Folder, dataSource.manager);
|
||||
}
|
||||
|
||||
async getMany(options: ListQuery.Options = {}): Promise<[Folder[], number]> {
|
||||
const query = this.createQueryBuilder('folder');
|
||||
|
||||
this.applySelections(query, options.select);
|
||||
this.applyFilters(query, options.filter);
|
||||
this.applySorting(query, options.sortBy);
|
||||
this.applyPagination(query, options);
|
||||
|
||||
return await query.getManyAndCount();
|
||||
}
|
||||
|
||||
private applySelections(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
select?: Record<string, boolean>,
|
||||
): void {
|
||||
if (select) {
|
||||
this.applyCustomSelect(query, select);
|
||||
} else {
|
||||
this.applyDefaultSelect(query);
|
||||
}
|
||||
}
|
||||
|
||||
private applyDefaultSelect(query: SelectQueryBuilder<Folder>): void {
|
||||
query
|
||||
.leftJoinAndSelect('folder.project', 'project')
|
||||
.leftJoinAndSelect('folder.parentFolder', 'parentFolder')
|
||||
.leftJoinAndSelect('folder.tags', 'tags')
|
||||
.leftJoinAndSelect('folder.workflows', 'workflows')
|
||||
.select([
|
||||
'folder',
|
||||
...this.getProjectFields('project'),
|
||||
...this.getTagFields(),
|
||||
...this.getParentFolderFields('parentFolder'),
|
||||
'workflows.id',
|
||||
]);
|
||||
}
|
||||
|
||||
private applyCustomSelect(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
select?: Record<string, boolean>,
|
||||
): void {
|
||||
const selections = ['folder.id'];
|
||||
|
||||
this.addBasicFields(selections, select);
|
||||
this.addRelationFields(query, selections, select);
|
||||
|
||||
query.select(selections);
|
||||
}
|
||||
|
||||
private addBasicFields(selections: string[], select?: Record<string, boolean>): void {
|
||||
if (select?.name) selections.push('folder.name');
|
||||
if (select?.createdAt) selections.push('folder.createdAt');
|
||||
if (select?.updatedAt) selections.push('folder.updatedAt');
|
||||
}
|
||||
|
||||
private addRelationFields(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
selections: string[],
|
||||
select?: Record<string, boolean>,
|
||||
): void {
|
||||
if (select?.project) {
|
||||
query.leftJoin('folder.project', 'project');
|
||||
selections.push(...this.getProjectFields('project'));
|
||||
}
|
||||
|
||||
if (select?.tags) {
|
||||
query.leftJoin('folder.tags', 'tags').addOrderBy('tags.createdAt', 'ASC');
|
||||
selections.push(...this.getTagFields());
|
||||
}
|
||||
|
||||
if (select?.parentFolder) {
|
||||
query.leftJoin('folder.parentFolder', 'parentFolder');
|
||||
selections.push(...this.getParentFolderFields('parentFolder'));
|
||||
}
|
||||
|
||||
if (select?.workflows) {
|
||||
query.leftJoinAndSelect('folder.workflows', 'workflows');
|
||||
selections.push('workflows.id');
|
||||
}
|
||||
}
|
||||
|
||||
private getProjectFields(alias: string): string[] {
|
||||
return [`${alias}.id`, `${alias}.name`, `${alias}.type`, `${alias}.icon`];
|
||||
}
|
||||
|
||||
private getTagFields(): string[] {
|
||||
return ['tags.id', 'tags.name'];
|
||||
}
|
||||
|
||||
private getParentFolderFields(alias: string): string[] {
|
||||
return [`${alias}.id`, `${alias}.name`];
|
||||
}
|
||||
|
||||
private applyFilters(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
filter?: ListQuery.Options['filter'],
|
||||
): void {
|
||||
if (!filter) return;
|
||||
|
||||
this.applyBasicFilters(query, filter);
|
||||
this.applyTagsFilter(query, Array.isArray(filter?.tags) ? filter.tags : undefined);
|
||||
}
|
||||
|
||||
private applyBasicFilters(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
filter: ListQuery.Options['filter'],
|
||||
): void {
|
||||
if (filter?.projectId) {
|
||||
query.andWhere('folder.projectId = :projectId', { projectId: filter.projectId });
|
||||
}
|
||||
|
||||
if (filter?.name && typeof filter.name === 'string') {
|
||||
query.andWhere('LOWER(folder.name) LIKE LOWER(:name)', {
|
||||
name: `%${filter.name}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (filter?.parentFolderId) {
|
||||
query.andWhere('folder.parentFolderId = :parentFolderId', {
|
||||
parentFolderId: filter.parentFolderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private applyTagsFilter(query: SelectQueryBuilder<Folder>, tags?: string[]): void {
|
||||
if (!Array.isArray(tags) || tags.length === 0) return;
|
||||
|
||||
const subQuery = this.createTagsSubQuery(query, tags);
|
||||
|
||||
query.andWhere(`folder.id IN (${subQuery.getQuery()})`).setParameters({
|
||||
tagNames: tags,
|
||||
tagCount: tags.length,
|
||||
});
|
||||
}
|
||||
|
||||
private createTagsSubQuery(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
tags: string[],
|
||||
): SelectQueryBuilder<FolderTagMapping> {
|
||||
return query
|
||||
.subQuery()
|
||||
.select('ft.folderId')
|
||||
.from(FolderTagMapping, 'ft')
|
||||
.innerJoin(TagEntity, 'filter_tags', 'filter_tags.id = ft.tagId')
|
||||
.where('filter_tags.name IN (:...tagNames)', { tagNames: tags })
|
||||
.groupBy('ft.folderId')
|
||||
.having('COUNT(DISTINCT filter_tags.name) = :tagCount', {
|
||||
tagCount: tags.length,
|
||||
});
|
||||
}
|
||||
|
||||
private applySorting(query: SelectQueryBuilder<Folder>, sortBy?: string): void {
|
||||
if (!sortBy) {
|
||||
query.orderBy('folder.updatedAt', 'DESC');
|
||||
return;
|
||||
}
|
||||
|
||||
const [field, order] = this.parseSortingParams(sortBy);
|
||||
this.applySortingByField(query, field, order);
|
||||
}
|
||||
|
||||
private parseSortingParams(sortBy: string): [string, 'DESC' | 'ASC'] {
|
||||
const [field, order] = sortBy.split(':');
|
||||
return [field, order?.toLowerCase() === 'desc' ? 'DESC' : 'ASC'];
|
||||
}
|
||||
|
||||
private applySortingByField(
|
||||
query: SelectQueryBuilder<Folder>,
|
||||
field: string,
|
||||
direction: 'DESC' | 'ASC',
|
||||
): void {
|
||||
if (field === 'name') {
|
||||
query.orderBy('LOWER(folder.name)', direction);
|
||||
} else if (['createdAt', 'updatedAt'].includes(field)) {
|
||||
query.orderBy(`folder.${field}`, direction);
|
||||
}
|
||||
}
|
||||
|
||||
private applyPagination(query: SelectQueryBuilder<Folder>, options: ListQuery.Options): void {
|
||||
if (options?.take) {
|
||||
query.skip(options.skip ?? 0).take(options.take);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { TagEntity } from '@/databases/entities/tag-entity';
|
||||
import type { AssignableRole, GlobalRole, User } from '@/databases/entities/user';
|
||||
|
||||
import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants';
|
||||
import type { Folder } from './databases/entities/folder';
|
||||
import type { ExternalHooks } from './external-hooks';
|
||||
import type { WorkflowWithSharingsAndCredentials } from './workflows/workflows.types';
|
||||
|
||||
@@ -92,6 +93,7 @@ export type IAnnotationTagWithCountDb = IAnnotationTagDb & UsageCount;
|
||||
export interface IWorkflowDb extends IWorkflowBase {
|
||||
triggerCount: number;
|
||||
tags?: TagEntity[];
|
||||
parentFolder?: Folder | null;
|
||||
}
|
||||
|
||||
export interface IWorkflowToImport extends IWorkflowBase {
|
||||
|
||||
32
packages/cli/test/integration/shared/db/folders.ts
Normal file
32
packages/cli/test/integration/shared/db/folders.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import type { Folder } from '@/databases/entities/folder';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { TagEntity } from '@/databases/entities/tag-entity';
|
||||
import { FolderRepository } from '@/databases/repositories/folder.repository';
|
||||
import { randomName } from '@test-integration/random';
|
||||
|
||||
export const createFolder = async (
|
||||
project: Project,
|
||||
options: {
|
||||
name?: string;
|
||||
parentFolder?: Folder;
|
||||
tags?: TagEntity[];
|
||||
updatedAt?: Date;
|
||||
createdAt?: Date;
|
||||
} = {},
|
||||
) => {
|
||||
const folderRepository = Container.get(FolderRepository);
|
||||
const folder = await folderRepository.save(
|
||||
folderRepository.create({
|
||||
name: options.name ?? randomName(),
|
||||
project,
|
||||
parentFolder: options.parentFolder ?? null,
|
||||
tags: options.tags ?? [],
|
||||
updatedAt: options.updatedAt ?? new Date(),
|
||||
createdAt: options.updatedAt ?? new Date(),
|
||||
}),
|
||||
);
|
||||
|
||||
return folder;
|
||||
};
|
||||
@@ -85,6 +85,7 @@ const repositories = [
|
||||
'WorkflowStatistics',
|
||||
'WorkflowTagMapping',
|
||||
'ApiKey',
|
||||
'Folder',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user