mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Introduce new query string parameter includeFolders
to GET /workflows
endpoint (no-changelog) (#13352)
This commit is contained in:
parent
243042f217
commit
c3f111275b
|
@ -13,6 +13,10 @@ import { Project } from './project';
|
||||||
import { TagEntity } from './tag-entity';
|
import { TagEntity } from './tag-entity';
|
||||||
import { type WorkflowEntity } from './workflow-entity';
|
import { type WorkflowEntity } from './workflow-entity';
|
||||||
|
|
||||||
|
export type FolderWithWorkflowsCount = Folder & {
|
||||||
|
workflowsCount: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Folder extends WithTimestampsAndStringId {
|
export class Folder extends WithTimestampsAndStringId {
|
||||||
@Column()
|
@Column()
|
||||||
|
@ -24,7 +28,7 @@ export class Folder extends WithTimestampsAndStringId {
|
||||||
|
|
||||||
@ManyToOne(() => Project)
|
@ManyToOne(() => Project)
|
||||||
@JoinColumn({ name: 'projectId' })
|
@JoinColumn({ name: 'projectId' })
|
||||||
project: Project;
|
homeProject: Project;
|
||||||
|
|
||||||
@OneToMany('WorkflowEntity', 'parentFolder')
|
@OneToMany('WorkflowEntity', 'parentFolder')
|
||||||
workflows: WorkflowEntity[];
|
workflows: WorkflowEntity[];
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { DateTime } from 'luxon';
|
||||||
import type { Folder } from '@/databases/entities/folder';
|
import type { Folder } from '@/databases/entities/folder';
|
||||||
import type { Project } from '@/databases/entities/project';
|
import type { Project } from '@/databases/entities/project';
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
|
||||||
import { createFolder } from '@test-integration/db/folders';
|
import { createFolder } from '@test-integration/db/folders';
|
||||||
import { getPersonalProject } from '@test-integration/db/projects';
|
import { getPersonalProject } from '@test-integration/db/projects';
|
||||||
import { createTag } from '@test-integration/db/tags';
|
import { createTag } from '@test-integration/db/tags';
|
||||||
|
@ -46,7 +45,7 @@ describe('FolderRepository', () => {
|
||||||
|
|
||||||
await Promise.all([folder1, folder2]);
|
await Promise.all([folder1, folder2]);
|
||||||
|
|
||||||
const [folders, count] = await folderRepository.getMany();
|
const [folders, count] = await folderRepository.getManyAndCount();
|
||||||
expect(count).toBe(2);
|
expect(count).toBe(2);
|
||||||
expect(folders).toHaveLength(2);
|
expect(folders).toHaveLength(2);
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ describe('FolderRepository', () => {
|
||||||
createdAt: expect.any(Date),
|
createdAt: expect.any(Date),
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
parentFolder: null,
|
parentFolder: null,
|
||||||
project: {
|
homeProject: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
type: expect.any(String),
|
type: expect.any(String),
|
||||||
|
@ -70,6 +69,23 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should filter folders by IDs', async () => {
|
||||||
|
const anotherUser = await createMember();
|
||||||
|
const anotherProject = await getPersonalProject(anotherUser);
|
||||||
|
|
||||||
|
const folder1 = await createFolder(project, { name: 'folder1' });
|
||||||
|
await createFolder(anotherProject, { name: 'folder2' });
|
||||||
|
|
||||||
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
|
filter: { folderIds: [folder1.id] },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(count).toBe(1);
|
||||||
|
expect(folders).toHaveLength(1);
|
||||||
|
expect(folders[0].name).toBe('folder1');
|
||||||
|
});
|
||||||
|
|
||||||
it('should filter folders by project ID', async () => {
|
it('should filter folders by project ID', async () => {
|
||||||
const anotherUser = await createMember();
|
const anotherUser = await createMember();
|
||||||
const anotherProject = await getPersonalProject(anotherUser);
|
const anotherProject = await getPersonalProject(anotherUser);
|
||||||
|
@ -79,14 +95,14 @@ describe('FolderRepository', () => {
|
||||||
|
|
||||||
await Promise.all([folder1, folder2]);
|
await Promise.all([folder1, folder2]);
|
||||||
|
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
filter: { projectId: project.id },
|
filter: { projectId: project.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(count).toBe(1);
|
expect(count).toBe(1);
|
||||||
expect(folders).toHaveLength(1);
|
expect(folders).toHaveLength(1);
|
||||||
expect(folders[0].name).toBe('folder1');
|
expect(folders[0].name).toBe('folder1');
|
||||||
expect(folders[0].project.id).toBe(project.id);
|
expect(folders[0].homeProject.id).toBe(project.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter folders by name case-insensitively', async () => {
|
it('should filter folders by name case-insensitively', async () => {
|
||||||
|
@ -96,7 +112,7 @@ describe('FolderRepository', () => {
|
||||||
|
|
||||||
await Promise.all([folder1, folder2, folder3]);
|
await Promise.all([folder1, folder2, folder3]);
|
||||||
|
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
filter: { name: 'test' },
|
filter: { name: 'test' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,6 +122,8 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter folders by parent folder ID', async () => {
|
it('should filter folders by parent folder ID', async () => {
|
||||||
|
let folders: Folder[];
|
||||||
|
let count: number;
|
||||||
const parentFolder = await createFolder(project, { name: 'Parent' });
|
const parentFolder = await createFolder(project, { name: 'Parent' });
|
||||||
await createFolder(project, {
|
await createFolder(project, {
|
||||||
name: 'Child 1',
|
name: 'Child 1',
|
||||||
|
@ -117,7 +135,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
await createFolder(project, { name: 'Unrelated' });
|
await createFolder(project, { name: 'Unrelated' });
|
||||||
|
|
||||||
const [folders, count] = await folderRepository.getMany({
|
[folders, count] = await folderRepository.getManyAndCount({
|
||||||
filter: { parentFolderId: parentFolder.id },
|
filter: { parentFolderId: parentFolder.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -127,6 +145,17 @@ describe('FolderRepository', () => {
|
||||||
folders.forEach((folder) => {
|
folders.forEach((folder) => {
|
||||||
expect(folder.parentFolder?.id).toBe(parentFolder.id);
|
expect(folder.parentFolder?.id).toBe(parentFolder.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[folders, count] = await folderRepository.getManyAndCount({
|
||||||
|
filter: { parentFolderId: '0' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(count).toBe(2);
|
||||||
|
expect(folders).toHaveLength(2);
|
||||||
|
expect(folders.map((f) => f.name).sort()).toEqual(['Parent', 'Unrelated']);
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
expect(folder.parentFolder).toBe(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter folders by a single tag', async () => {
|
it('should filter folders by a single tag', async () => {
|
||||||
|
@ -143,7 +172,7 @@ describe('FolderRepository', () => {
|
||||||
tags: [tag2],
|
tags: [tag2],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
filter: { tags: ['important'] },
|
filter: { tags: ['important'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -171,7 +200,7 @@ describe('FolderRepository', () => {
|
||||||
tags: [tag3],
|
tags: [tag3],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
filter: { tags: ['important', 'active'] },
|
filter: { tags: ['important', 'active'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -199,7 +228,7 @@ describe('FolderRepository', () => {
|
||||||
tags: [tag2],
|
tags: [tag2],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
filter: {
|
filter: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
parentFolderId: parentFolder.id,
|
parentFolderId: parentFolder.id,
|
||||||
|
@ -217,7 +246,6 @@ describe('FolderRepository', () => {
|
||||||
|
|
||||||
describe('select', () => {
|
describe('select', () => {
|
||||||
let testFolder: Folder;
|
let testFolder: Folder;
|
||||||
let workflowWithTestFolder: WorkflowEntity;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const parentFolder = await createFolder(project, { name: 'Parent Folder' });
|
const parentFolder = await createFolder(project, { name: 'Parent Folder' });
|
||||||
|
@ -227,11 +255,11 @@ describe('FolderRepository', () => {
|
||||||
parentFolder,
|
parentFolder,
|
||||||
tags: [tag],
|
tags: [tag],
|
||||||
});
|
});
|
||||||
workflowWithTestFolder = await createWorkflow({ parentFolder: testFolder });
|
await createWorkflow({ parentFolder: testFolder });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select only id and name when specified', async () => {
|
it('should select only id and name when specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -251,7 +279,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return id, name and tags when specified', async () => {
|
it('should return id, name and tags when specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -276,7 +304,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return id, name and project when specified', async () => {
|
it('should return id, name and project when specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -286,8 +314,8 @@ describe('FolderRepository', () => {
|
||||||
|
|
||||||
expect(folders).toHaveLength(2);
|
expect(folders).toHaveLength(2);
|
||||||
folders.forEach((folder) => {
|
folders.forEach((folder) => {
|
||||||
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'project']);
|
expect(Object.keys(folder).sort()).toEqual(['homeProject', 'id', 'name']);
|
||||||
expect(folder.project).toEqual({
|
expect(folder.homeProject).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
type: expect.any(String),
|
type: expect.any(String),
|
||||||
|
@ -297,7 +325,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return id, name and parentFolder when specified', async () => {
|
it('should return id, name and parentFolder when specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -320,29 +348,26 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return id, name and workflows when specified', async () => {
|
it('should return id, name and workflowsCount when specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
workflows: true,
|
workflowsCount: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(folders).toHaveLength(2);
|
expect(folders).toHaveLength(2);
|
||||||
folders.forEach((folder) => {
|
folders.forEach((folder) => {
|
||||||
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'workflows']);
|
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'workflowsCount']);
|
||||||
expect(folder.id).toBeDefined();
|
expect(folder.id).toBeDefined();
|
||||||
expect(folder.name).toBeDefined();
|
expect(folder.name).toBeDefined();
|
||||||
expect(Array.isArray(folder.workflows)).toBeTruthy();
|
expect(folder.workflowsCount).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(folders[0].workflows).toHaveLength(1);
|
|
||||||
expect(folders[0].workflows[0].id).toBe(workflowWithTestFolder.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return timestamps when specified', async () => {
|
it('should return timestamps when specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
@ -359,7 +384,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all properties when no select is specified', async () => {
|
it('should return all properties when no select is specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany();
|
const [folders] = await folderRepository.getManyAndCount();
|
||||||
|
|
||||||
expect(folders).toHaveLength(2);
|
expect(folders).toHaveLength(2);
|
||||||
folders.forEach((folder) => {
|
folders.forEach((folder) => {
|
||||||
|
@ -368,13 +393,13 @@ describe('FolderRepository', () => {
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
createdAt: expect.any(Date),
|
createdAt: expect.any(Date),
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
project: {
|
homeProject: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
type: expect.any(String),
|
type: expect.any(String),
|
||||||
icon: null,
|
icon: null,
|
||||||
},
|
},
|
||||||
workflows: expect.any(Array),
|
workflowsCount: expect.any(Number),
|
||||||
tags: expect.any(Array),
|
tags: expect.any(Array),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -408,7 +433,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should limit results when take is specified', async () => {
|
it('should limit results when take is specified', async () => {
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
take: 3,
|
take: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -417,7 +442,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip results when skip is specified', async () => {
|
it('should skip results when skip is specified', async () => {
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
skip: 2,
|
skip: 2,
|
||||||
take: 5,
|
take: 5,
|
||||||
});
|
});
|
||||||
|
@ -428,7 +453,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle skip and take together', async () => {
|
it('should handle skip and take together', async () => {
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
skip: 1,
|
skip: 1,
|
||||||
take: 2,
|
take: 2,
|
||||||
});
|
});
|
||||||
|
@ -439,7 +464,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle take larger than remaining items', async () => {
|
it('should handle take larger than remaining items', async () => {
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
skip: 3,
|
skip: 3,
|
||||||
take: 10,
|
take: 10,
|
||||||
});
|
});
|
||||||
|
@ -450,7 +475,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle zero take by returning all results', async () => {
|
it('should handle zero take by returning all results', async () => {
|
||||||
const [folders, count] = await folderRepository.getMany({
|
const [folders, count] = await folderRepository.getManyAndCount({
|
||||||
take: 0,
|
take: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -495,7 +520,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by default (updatedAt:desc)', async () => {
|
it('should sort by default (updatedAt:desc)', async () => {
|
||||||
const [folders] = await folderRepository.getMany();
|
const [folders] = await folderRepository.getManyAndCount();
|
||||||
|
|
||||||
expect(folders.map((f) => f.name)).toEqual([
|
expect(folders.map((f) => f.name)).toEqual([
|
||||||
'C Folder',
|
'C Folder',
|
||||||
|
@ -506,7 +531,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by name:asc', async () => {
|
it('should sort by name:asc', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'name:asc',
|
sortBy: 'name:asc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -519,7 +544,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by name:desc', async () => {
|
it('should sort by name:desc', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'name:desc',
|
sortBy: 'name:desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -532,7 +557,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by createdAt:asc', async () => {
|
it('should sort by createdAt:asc', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'createdAt:asc',
|
sortBy: 'createdAt:asc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -545,7 +570,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by createdAt:desc', async () => {
|
it('should sort by createdAt:desc', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'createdAt:desc',
|
sortBy: 'createdAt:desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -558,7 +583,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by updatedAt:asc', async () => {
|
it('should sort by updatedAt:asc', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'updatedAt:asc',
|
sortBy: 'updatedAt:asc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -571,7 +596,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort by updatedAt:desc', async () => {
|
it('should sort by updatedAt:desc', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'updatedAt:desc',
|
sortBy: 'updatedAt:desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -584,7 +609,7 @@ describe('FolderRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to asc if order not specified', async () => {
|
it('should default to asc if order not specified', async () => {
|
||||||
const [folders] = await folderRepository.getMany({
|
const [folders] = await folderRepository.getManyAndCount({
|
||||||
sortBy: 'name',
|
sortBy: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,30 @@ import { DataSource, Repository } from '@n8n/typeorm';
|
||||||
|
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
|
|
||||||
|
import type { FolderWithWorkflowsCount } from '../entities/folder';
|
||||||
import { Folder } from '../entities/folder';
|
import { Folder } from '../entities/folder';
|
||||||
import { FolderTagMapping } from '../entities/folder-tag-mapping';
|
import { FolderTagMapping } from '../entities/folder-tag-mapping';
|
||||||
import { TagEntity } from '../entities/tag-entity';
|
import { TagEntity } from '../entities/tag-entity';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class FolderRepository extends Repository<Folder> {
|
export class FolderRepository extends Repository<FolderWithWorkflowsCount> {
|
||||||
constructor(dataSource: DataSource) {
|
constructor(dataSource: DataSource) {
|
||||||
super(Folder, dataSource.manager);
|
super(Folder, dataSource.manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMany(options: ListQuery.Options = {}): Promise<[Folder[], number]> {
|
async getManyAndCount(
|
||||||
|
options: ListQuery.Options = {},
|
||||||
|
): Promise<[FolderWithWorkflowsCount[], number]> {
|
||||||
|
const query = this.getManyQuery(options);
|
||||||
|
return await query.getManyAndCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMany(options: ListQuery.Options = {}): Promise<FolderWithWorkflowsCount[]> {
|
||||||
|
const query = this.getManyQuery(options);
|
||||||
|
return await query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
getManyQuery(options: ListQuery.Options = {}): SelectQueryBuilder<FolderWithWorkflowsCount> {
|
||||||
const query = this.createQueryBuilder('folder');
|
const query = this.createQueryBuilder('folder');
|
||||||
|
|
||||||
this.applySelections(query, options.select);
|
this.applySelections(query, options.select);
|
||||||
|
@ -22,11 +35,11 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
this.applySorting(query, options.sortBy);
|
this.applySorting(query, options.sortBy);
|
||||||
this.applyPagination(query, options);
|
this.applyPagination(query, options);
|
||||||
|
|
||||||
return await query.getManyAndCount();
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySelections(
|
private applySelections(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
select?: Record<string, boolean>,
|
select?: Record<string, boolean>,
|
||||||
): void {
|
): void {
|
||||||
if (select) {
|
if (select) {
|
||||||
|
@ -36,23 +49,22 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyDefaultSelect(query: SelectQueryBuilder<Folder>): void {
|
private applyDefaultSelect(query: SelectQueryBuilder<FolderWithWorkflowsCount>): void {
|
||||||
query
|
query
|
||||||
.leftJoinAndSelect('folder.project', 'project')
|
.leftJoinAndSelect('folder.homeProject', 'homeProject')
|
||||||
.leftJoinAndSelect('folder.parentFolder', 'parentFolder')
|
.leftJoinAndSelect('folder.parentFolder', 'parentFolder')
|
||||||
.leftJoinAndSelect('folder.tags', 'tags')
|
.leftJoinAndSelect('folder.tags', 'tags')
|
||||||
.leftJoinAndSelect('folder.workflows', 'workflows')
|
.loadRelationCountAndMap('folder.workflowsCount', 'folder.workflows')
|
||||||
.select([
|
.select([
|
||||||
'folder',
|
'folder',
|
||||||
...this.getProjectFields('project'),
|
...this.getProjectFields('homeProject'),
|
||||||
...this.getTagFields(),
|
...this.getTagFields(),
|
||||||
...this.getParentFolderFields('parentFolder'),
|
...this.getParentFolderFields('parentFolder'),
|
||||||
'workflows.id',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCustomSelect(
|
private applyCustomSelect(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
select?: Record<string, boolean>,
|
select?: Record<string, boolean>,
|
||||||
): void {
|
): void {
|
||||||
const selections = ['folder.id'];
|
const selections = ['folder.id'];
|
||||||
|
@ -70,13 +82,13 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private addRelationFields(
|
private addRelationFields(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
selections: string[],
|
selections: string[],
|
||||||
select?: Record<string, boolean>,
|
select?: Record<string, boolean>,
|
||||||
): void {
|
): void {
|
||||||
if (select?.project) {
|
if (select?.project) {
|
||||||
query.leftJoin('folder.project', 'project');
|
query.leftJoin('folder.homeProject', 'homeProject');
|
||||||
selections.push(...this.getProjectFields('project'));
|
selections.push(...this.getProjectFields('homeProject'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (select?.tags) {
|
if (select?.tags) {
|
||||||
|
@ -89,9 +101,8 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
selections.push(...this.getParentFolderFields('parentFolder'));
|
selections.push(...this.getParentFolderFields('parentFolder'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (select?.workflows) {
|
if (select?.workflowsCount) {
|
||||||
query.leftJoinAndSelect('folder.workflows', 'workflows');
|
query.loadRelationCountAndMap('folder.workflowsCount', 'folder.workflows');
|
||||||
selections.push('workflows.id');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +119,7 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyFilters(
|
private applyFilters(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
filter?: ListQuery.Options['filter'],
|
filter?: ListQuery.Options['filter'],
|
||||||
): void {
|
): void {
|
||||||
if (!filter) return;
|
if (!filter) return;
|
||||||
|
@ -118,9 +129,19 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyBasicFilters(
|
private applyBasicFilters(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
filter: ListQuery.Options['filter'],
|
filter: ListQuery.Options['filter'],
|
||||||
): void {
|
): void {
|
||||||
|
if (filter?.folderIds && Array.isArray(filter.folderIds)) {
|
||||||
|
query.andWhere('folder.id IN (:...folderIds)', {
|
||||||
|
/*
|
||||||
|
* If folderIds is empty, add a dummy value to prevent an error
|
||||||
|
* when using the IN operator with an empty array.
|
||||||
|
*/
|
||||||
|
folderIds: !filter?.folderIds.length ? [''] : filter?.folderIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (filter?.projectId) {
|
if (filter?.projectId) {
|
||||||
query.andWhere('folder.projectId = :projectId', { projectId: filter.projectId });
|
query.andWhere('folder.projectId = :projectId', { projectId: filter.projectId });
|
||||||
}
|
}
|
||||||
|
@ -131,14 +152,19 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter?.parentFolderId) {
|
if (filter?.parentFolderId === '0') {
|
||||||
|
query.andWhere('folder.parentFolderId IS NULL');
|
||||||
|
} else if (filter?.parentFolderId) {
|
||||||
query.andWhere('folder.parentFolderId = :parentFolderId', {
|
query.andWhere('folder.parentFolderId = :parentFolderId', {
|
||||||
parentFolderId: filter.parentFolderId,
|
parentFolderId: filter.parentFolderId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyTagsFilter(query: SelectQueryBuilder<Folder>, tags?: string[]): void {
|
private applyTagsFilter(
|
||||||
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
|
tags?: string[],
|
||||||
|
): void {
|
||||||
if (!Array.isArray(tags) || tags.length === 0) return;
|
if (!Array.isArray(tags) || tags.length === 0) return;
|
||||||
|
|
||||||
const subQuery = this.createTagsSubQuery(query, tags);
|
const subQuery = this.createTagsSubQuery(query, tags);
|
||||||
|
@ -150,7 +176,7 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTagsSubQuery(
|
private createTagsSubQuery(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
): SelectQueryBuilder<FolderTagMapping> {
|
): SelectQueryBuilder<FolderTagMapping> {
|
||||||
return query
|
return query
|
||||||
|
@ -165,7 +191,7 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySorting(query: SelectQueryBuilder<Folder>, sortBy?: string): void {
|
private applySorting(query: SelectQueryBuilder<FolderWithWorkflowsCount>, sortBy?: string): void {
|
||||||
if (!sortBy) {
|
if (!sortBy) {
|
||||||
query.orderBy('folder.updatedAt', 'DESC');
|
query.orderBy('folder.updatedAt', 'DESC');
|
||||||
return;
|
return;
|
||||||
|
@ -181,7 +207,7 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySortingByField(
|
private applySortingByField(
|
||||||
query: SelectQueryBuilder<Folder>,
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
field: string,
|
field: string,
|
||||||
direction: 'DESC' | 'ASC',
|
direction: 'DESC' | 'ASC',
|
||||||
): void {
|
): void {
|
||||||
|
@ -192,7 +218,10 @@ export class FolderRepository extends Repository<Folder> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyPagination(query: SelectQueryBuilder<Folder>, options: ListQuery.Options): void {
|
private applyPagination(
|
||||||
|
query: SelectQueryBuilder<FolderWithWorkflowsCount>,
|
||||||
|
options: ListQuery.Options,
|
||||||
|
): void {
|
||||||
if (options?.take) {
|
if (options?.take) {
|
||||||
query.skip(options.skip ?? 0).take(options.take);
|
query.skip(options.skip ?? 0).take(options.take);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,38 @@ import type {
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
import { isStringArray } from '@/utils';
|
import { isStringArray } from '@/utils';
|
||||||
|
|
||||||
|
import { FolderRepository } from './folder.repository';
|
||||||
|
import type { Folder, FolderWithWorkflowsCount } from '../entities/folder';
|
||||||
import { TagEntity } from '../entities/tag-entity';
|
import { TagEntity } from '../entities/tag-entity';
|
||||||
import { WebhookEntity } from '../entities/webhook-entity';
|
import { WebhookEntity } from '../entities/webhook-entity';
|
||||||
import { WorkflowEntity } from '../entities/workflow-entity';
|
import { WorkflowEntity } from '../entities/workflow-entity';
|
||||||
import { WorkflowTagMapping } from '../entities/workflow-tag-mapping';
|
import { WorkflowTagMapping } from '../entities/workflow-tag-mapping';
|
||||||
|
|
||||||
|
type ResourceType = 'folder' | 'workflow';
|
||||||
|
|
||||||
|
type WorkflowFolderUnionRow = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
name_lower?: string;
|
||||||
|
resource: ResourceType;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowFolderUnionFull = (
|
||||||
|
| ListQuery.Workflow.Plain
|
||||||
|
| ListQuery.Workflow.WithSharing
|
||||||
|
| FolderWithWorkflowsCount
|
||||||
|
) & {
|
||||||
|
resource: ResourceType;
|
||||||
|
};
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class WorkflowRepository extends Repository<WorkflowEntity> {
|
export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
constructor(
|
constructor(
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
|
private readonly folderRepository: FolderRepository,
|
||||||
) {
|
) {
|
||||||
super(WorkflowEntity, dataSource.manager);
|
super(WorkflowEntity, dataSource.manager);
|
||||||
}
|
}
|
||||||
|
@ -99,20 +121,218 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMany(sharedWorkflowIds: string[], options: ListQuery.Options = {}) {
|
private buildBaseUnionQuery(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
|
const subQueryParameters: ListQuery.Options = {
|
||||||
|
select: {
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
filter: options.filter,
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnNames = [...Object.keys(subQueryParameters.select ?? {}), 'resource'];
|
||||||
|
|
||||||
|
const [sortByColumn, sortByDirection] = this.parseSortingParams(
|
||||||
|
options.sortBy ?? 'updatedAt:asc',
|
||||||
|
);
|
||||||
|
|
||||||
|
const foldersQuery = this.folderRepository
|
||||||
|
.getManyQuery(subQueryParameters)
|
||||||
|
.addSelect("'folder'", 'resource');
|
||||||
|
|
||||||
|
const workflowsQuery = this.getManyQuery(workflowIds, subQueryParameters).addSelect(
|
||||||
|
"'workflow'",
|
||||||
|
'resource',
|
||||||
|
);
|
||||||
|
|
||||||
|
const qb = this.manager.createQueryBuilder();
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseQuery: qb
|
||||||
|
.createQueryBuilder()
|
||||||
|
.addCommonTableExpression(foldersQuery, 'FOLDERS_QUERY', { columnNames })
|
||||||
|
.addCommonTableExpression(workflowsQuery, 'WORKFLOWS_QUERY', { columnNames })
|
||||||
|
.addCommonTableExpression(
|
||||||
|
`SELECT * FROM ${qb.escape('FOLDERS_QUERY')} UNION ALL SELECT * FROM ${qb.escape('WORKFLOWS_QUERY')}`,
|
||||||
|
'RESULT_QUERY',
|
||||||
|
),
|
||||||
|
sortByColumn,
|
||||||
|
sortByDirection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkflowsAndFoldersUnion(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
|
const { baseQuery, sortByColumn, sortByDirection } = this.buildBaseUnionQuery(
|
||||||
|
workflowIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = this.buildUnionQuery(baseQuery, {
|
||||||
|
sortByColumn,
|
||||||
|
sortByDirection,
|
||||||
|
pagination: {
|
||||||
|
take: options.take,
|
||||||
|
skip: options.skip ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowsAndFolders = await query.getRawMany<WorkflowFolderUnionRow>();
|
||||||
|
return this.removeNameLowerFromResults(workflowsAndFolders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUnionQuery(
|
||||||
|
baseQuery: SelectQueryBuilder<any>,
|
||||||
|
options: {
|
||||||
|
sortByColumn: string;
|
||||||
|
sortByDirection: 'ASC' | 'DESC';
|
||||||
|
pagination: {
|
||||||
|
take?: number;
|
||||||
|
skip: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const query = baseQuery
|
||||||
|
.select(`${baseQuery.escape('RESULT')}.*`)
|
||||||
|
.from('RESULT_QUERY', 'RESULT');
|
||||||
|
|
||||||
|
this.applySortingToUnionQuery(query, baseQuery, options);
|
||||||
|
this.applyPaginationToUnionQuery(query, options.pagination);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applySortingToUnionQuery(
|
||||||
|
query: SelectQueryBuilder<any>,
|
||||||
|
baseQuery: SelectQueryBuilder<any>,
|
||||||
|
options: { sortByColumn: string; sortByDirection: 'ASC' | 'DESC' },
|
||||||
|
) {
|
||||||
|
const { sortByColumn, sortByDirection } = options;
|
||||||
|
|
||||||
|
const resultTableEscaped = baseQuery.escape('RESULT');
|
||||||
|
const nameColumnEscaped = baseQuery.escape('name');
|
||||||
|
const resourceColumnEscaped = baseQuery.escape('resource');
|
||||||
|
const sortByColumnEscaped = baseQuery.escape(sortByColumn);
|
||||||
|
|
||||||
|
// Guarantee folders show up first
|
||||||
|
query.orderBy(`${resultTableEscaped}.${resourceColumnEscaped}`, 'ASC');
|
||||||
|
|
||||||
|
if (sortByColumn === 'name') {
|
||||||
|
query
|
||||||
|
.addSelect(`LOWER(${resultTableEscaped}.${nameColumnEscaped})`, 'name_lower')
|
||||||
|
.addOrderBy('name_lower', sortByDirection);
|
||||||
|
} else {
|
||||||
|
query.addOrderBy(`${resultTableEscaped}.${sortByColumnEscaped}`, sortByDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyPaginationToUnionQuery(
|
||||||
|
query: SelectQueryBuilder<any>,
|
||||||
|
pagination: { take?: number; skip: number },
|
||||||
|
) {
|
||||||
|
if (pagination.take) {
|
||||||
|
query.take(pagination.take);
|
||||||
|
}
|
||||||
|
query.skip(pagination.skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeNameLowerFromResults(results: WorkflowFolderUnionRow[]) {
|
||||||
|
return results.map(({ name_lower, ...rest }) => rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkflowsAndFoldersCount(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
|
const { skip, take, ...baseQueryParameters } = options;
|
||||||
|
|
||||||
|
const { baseQuery } = this.buildBaseUnionQuery(workflowIds, baseQueryParameters);
|
||||||
|
|
||||||
|
const response = await baseQuery
|
||||||
|
.select(`COUNT(DISTINCT ${baseQuery.escape('RESULT')}.${baseQuery.escape('id')})`, 'count')
|
||||||
|
.from('RESULT_QUERY', 'RESULT')
|
||||||
|
.select('COUNT(*)', 'count')
|
||||||
|
.getRawOne<{ count: number | string }>();
|
||||||
|
|
||||||
|
return Number(response?.count) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkflowsAndFoldersWithCount(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
|
const [workflowsAndFolders, count] = await Promise.all([
|
||||||
|
this.getWorkflowsAndFoldersUnion(workflowIds, options),
|
||||||
|
this.getWorkflowsAndFoldersCount(workflowIds, options),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { workflows, folders } = await this.fetchExtraData(workflowsAndFolders);
|
||||||
|
|
||||||
|
const enrichedWorkflowsAndFolders = this.enrichDataWithExtras(workflowsAndFolders, {
|
||||||
|
workflows,
|
||||||
|
folders,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [enrichedWorkflowsAndFolders, count] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFolderIds(workflowsAndFolders: WorkflowFolderUnionRow[]) {
|
||||||
|
return workflowsAndFolders.filter((item) => item.resource === 'folder').map((item) => item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWorkflowsIds(workflowsAndFolders: WorkflowFolderUnionRow[]) {
|
||||||
|
return workflowsAndFolders
|
||||||
|
.filter((item) => item.resource === 'workflow')
|
||||||
|
.map((item) => item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchExtraData(workflowsAndFolders: WorkflowFolderUnionRow[]) {
|
||||||
|
const workflowIds = this.getWorkflowsIds(workflowsAndFolders);
|
||||||
|
const folderIds = this.getFolderIds(workflowsAndFolders);
|
||||||
|
|
||||||
|
const [workflows, folders] = await Promise.all([
|
||||||
|
this.getMany(workflowIds),
|
||||||
|
this.folderRepository.getMany({ filter: { folderIds } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { workflows, folders };
|
||||||
|
}
|
||||||
|
|
||||||
|
private enrichDataWithExtras(
|
||||||
|
baseData: WorkflowFolderUnionRow[],
|
||||||
|
extraData: {
|
||||||
|
workflows: ListQuery.Workflow.WithSharing[] | ListQuery.Workflow.Plain[];
|
||||||
|
folders: Folder[];
|
||||||
|
},
|
||||||
|
): WorkflowFolderUnionFull[] {
|
||||||
|
const workflowsMap = new Map(extraData.workflows.map((workflow) => [workflow.id, workflow]));
|
||||||
|
const foldersMap = new Map(extraData.folders.map((folder) => [folder.id, folder]));
|
||||||
|
|
||||||
|
return baseData.map((item) => {
|
||||||
|
const lookupMap = item.resource === 'folder' ? foldersMap : workflowsMap;
|
||||||
|
const extraItem = lookupMap.get(item.id);
|
||||||
|
|
||||||
|
return extraItem ? { ...item, ...extraItem } : item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMany(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
|
if (workflowIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.getManyQuery(workflowIds, options);
|
||||||
|
|
||||||
|
const workflows = (await query.getMany()) as
|
||||||
|
| ListQuery.Workflow.Plain[]
|
||||||
|
| ListQuery.Workflow.WithSharing[];
|
||||||
|
|
||||||
|
return workflows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getManyAndCount(sharedWorkflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
if (sharedWorkflowIds.length === 0) {
|
if (sharedWorkflowIds.length === 0) {
|
||||||
return { workflows: [], count: 0 };
|
return { workflows: [], count: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const qb = this.createBaseQuery(sharedWorkflowIds);
|
const query = this.getManyQuery(sharedWorkflowIds, options);
|
||||||
|
|
||||||
this.applyFilters(qb, options.filter);
|
const [workflows, count] = (await query.getManyAndCount()) as [
|
||||||
this.applySelect(qb, options.select);
|
|
||||||
this.applyRelations(qb, options.select);
|
|
||||||
this.applySorting(qb, options.sortBy);
|
|
||||||
this.applyPagination(qb, options);
|
|
||||||
|
|
||||||
const [workflows, count] = (await qb.getManyAndCount()) as [
|
|
||||||
ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
|
ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
|
||||||
number,
|
number,
|
||||||
];
|
];
|
||||||
|
@ -120,9 +340,25 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
return { workflows, count };
|
return { workflows, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
private createBaseQuery(sharedWorkflowIds: string[]): SelectQueryBuilder<WorkflowEntity> {
|
getManyQuery(workflowIds: string[], options: ListQuery.Options = {}) {
|
||||||
return this.createQueryBuilder('workflow').where('workflow.id IN (:...sharedWorkflowIds)', {
|
const qb = this.createBaseQuery(workflowIds);
|
||||||
sharedWorkflowIds,
|
|
||||||
|
this.applyFilters(qb, options.filter);
|
||||||
|
this.applySelect(qb, options.select);
|
||||||
|
this.applyRelations(qb, options.select);
|
||||||
|
this.applySorting(qb, options.sortBy);
|
||||||
|
this.applyPagination(qb, options);
|
||||||
|
|
||||||
|
return qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBaseQuery(workflowIds: string[]): SelectQueryBuilder<WorkflowEntity> {
|
||||||
|
return this.createQueryBuilder('workflow').where('workflow.id IN (:...workflowIds)', {
|
||||||
|
/*
|
||||||
|
* If workflowIds is empty, add a dummy value to prevent an error
|
||||||
|
* when using the IN operator with an empty array.
|
||||||
|
*/
|
||||||
|
workflowIds: !workflowIds.length ? [''] : workflowIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,12 +366,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
qb: SelectQueryBuilder<WorkflowEntity>,
|
qb: SelectQueryBuilder<WorkflowEntity>,
|
||||||
filter?: ListQuery.Options['filter'],
|
filter?: ListQuery.Options['filter'],
|
||||||
): void {
|
): void {
|
||||||
if (!filter) return;
|
|
||||||
|
|
||||||
this.applyNameFilter(qb, filter);
|
this.applyNameFilter(qb, filter);
|
||||||
this.applyActiveFilter(qb, filter);
|
this.applyActiveFilter(qb, filter);
|
||||||
this.applyTagsFilter(qb, filter);
|
this.applyTagsFilter(qb, filter);
|
||||||
this.applyProjectFilter(qb, filter);
|
this.applyProjectFilter(qb, filter);
|
||||||
|
this.applyParentFolderFilter(qb, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyNameFilter(
|
private applyNameFilter(
|
||||||
|
@ -149,6 +384,19 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyParentFolderFilter(
|
||||||
|
qb: SelectQueryBuilder<WorkflowEntity>,
|
||||||
|
filter: ListQuery.Options['filter'],
|
||||||
|
): void {
|
||||||
|
if (filter?.parentFolderId === '0') {
|
||||||
|
qb.andWhere('workflow.parentFolderId IS NULL');
|
||||||
|
} else if (filter?.parentFolderId) {
|
||||||
|
qb.andWhere('workflow.parentFolderId = :parentFolderId', {
|
||||||
|
parentFolderId: filter.parentFolderId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applyActiveFilter(
|
private applyActiveFilter(
|
||||||
qb: SelectQueryBuilder<WorkflowEntity>,
|
qb: SelectQueryBuilder<WorkflowEntity>,
|
||||||
filter: ListQuery.Options['filter'],
|
filter: ListQuery.Options['filter'],
|
||||||
|
@ -219,12 +467,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
qb: SelectQueryBuilder<WorkflowEntity>,
|
qb: SelectQueryBuilder<WorkflowEntity>,
|
||||||
select?: Record<string, boolean>,
|
select?: Record<string, boolean>,
|
||||||
): void {
|
): void {
|
||||||
// Always start with workflow.id
|
|
||||||
qb.select(['workflow.id']);
|
|
||||||
|
|
||||||
if (!select) {
|
if (!select) {
|
||||||
// Default select fields when no select option provided
|
// Instead of selecting id first and then adding more fields,
|
||||||
qb.addSelect([
|
// select all fields at once
|
||||||
|
qb.select([
|
||||||
|
'workflow.id',
|
||||||
'workflow.name',
|
'workflow.name',
|
||||||
'workflow.active',
|
'workflow.active',
|
||||||
'workflow.createdAt',
|
'workflow.createdAt',
|
||||||
|
@ -234,17 +481,23 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For custom select, still start with ID but don't add it again
|
||||||
|
const fieldsToSelect = ['workflow.id'];
|
||||||
|
|
||||||
// Handle special fields separately
|
// Handle special fields separately
|
||||||
const regularFields = Object.entries(select).filter(
|
const regularFields = Object.entries(select).filter(
|
||||||
([field]) => !['ownedBy', 'tags'].includes(field),
|
([field]) => !['ownedBy', 'tags', 'parentFolder'].includes(field),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add regular fields
|
// Add regular fields
|
||||||
regularFields.forEach(([field, include]) => {
|
regularFields.forEach(([field, include]) => {
|
||||||
if (include) {
|
if (include && field !== 'id') {
|
||||||
qb.addSelect(`workflow.${field}`);
|
// Skip id since we already added it
|
||||||
|
fieldsToSelect.push(`workflow.${field}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
qb.select(fieldsToSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyRelations(
|
private applyRelations(
|
||||||
|
@ -255,6 +508,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
const isDefaultSelect = select === undefined;
|
const isDefaultSelect = select === undefined;
|
||||||
const areTagsRequested = isDefaultSelect || select?.tags;
|
const areTagsRequested = isDefaultSelect || select?.tags;
|
||||||
const isOwnedByIncluded = isDefaultSelect || select?.ownedBy;
|
const isOwnedByIncluded = isDefaultSelect || select?.ownedBy;
|
||||||
|
const isParentFolderIncluded = isDefaultSelect || select?.parentFolder;
|
||||||
|
|
||||||
|
if (isParentFolderIncluded) {
|
||||||
|
qb.leftJoinAndSelect('workflow.parentFolder', 'parentFolder');
|
||||||
|
}
|
||||||
|
|
||||||
if (areTagsEnabled && areTagsRequested) {
|
if (areTagsEnabled && areTagsRequested) {
|
||||||
this.applyTagsRelation(qb);
|
this.applyTagsRelation(qb);
|
||||||
|
@ -273,7 +531,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
|
|
||||||
private applySorting(qb: SelectQueryBuilder<WorkflowEntity>, sortBy?: string): void {
|
private applySorting(qb: SelectQueryBuilder<WorkflowEntity>, sortBy?: string): void {
|
||||||
if (!sortBy) {
|
if (!sortBy) {
|
||||||
this.applyDefaultSorting(qb);
|
qb.orderBy('workflow.updatedAt', 'ASC');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,10 +544,6 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||||
return [column, order.toUpperCase() as 'ASC' | 'DESC'];
|
return [column, order.toUpperCase() as 'ASC' | 'DESC'];
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyDefaultSorting(qb: SelectQueryBuilder<WorkflowEntity>): void {
|
|
||||||
qb.orderBy('workflow.updatedAt', 'ASC');
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySortingByColumn(
|
private applySortingByColumn(
|
||||||
qb: SelectQueryBuilder<WorkflowEntity>,
|
qb: SelectQueryBuilder<WorkflowEntity>,
|
||||||
column: string,
|
column: string,
|
||||||
|
|
|
@ -25,6 +25,11 @@ export class WorkflowFilter extends BaseFilter {
|
||||||
@Expose()
|
@Expose()
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Expose()
|
||||||
|
parentFolderId?: string;
|
||||||
|
|
||||||
static async fromString(rawFilter: string) {
|
static async fromString(rawFilter: string) {
|
||||||
return await this.toFilter(rawFilter, WorkflowFilter);
|
return await this.toFilter(rawFilter, WorkflowFilter);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export class WorkflowSelect extends BaseSelect {
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'versionId',
|
'versionId',
|
||||||
'ownedBy', // non-entity field
|
'ownedBy', // non-entity field
|
||||||
|
'parentFolder',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,12 @@ export declare namespace CredentialRequest {
|
||||||
|
|
||||||
type Get = AuthenticatedRequest<{ credentialId: string }, {}, {}, Record<string, string>>;
|
type Get = AuthenticatedRequest<{ credentialId: string }, {}, {}, Record<string, string>>;
|
||||||
|
|
||||||
type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & {
|
type GetMany = AuthenticatedRequest<
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
ListQuery.Params & { includeScopes?: string; includeFolders?: string }
|
||||||
|
> & {
|
||||||
listQueryOptions: ListQuery.Options;
|
listQueryOptions: ListQuery.Options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,12 @@ export declare namespace WorkflowRequest {
|
||||||
|
|
||||||
type Get = AuthenticatedRequest<{ workflowId: string }>;
|
type Get = AuthenticatedRequest<{ workflowId: string }>;
|
||||||
|
|
||||||
type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & {
|
type GetMany = AuthenticatedRequest<
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
ListQuery.Params & { includeScopes?: string; includeFolders?: string }
|
||||||
|
> & {
|
||||||
listQueryOptions: ListQuery.Options;
|
listQueryOptions: ListQuery.Options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||||
import { WorkflowTagMappingRepository } from '@/databases/repositories/workflow-tag-mapping.repository';
|
import { WorkflowTagMappingRepository } from '@/databases/repositories/workflow-tag-mapping.repository';
|
||||||
|
import type { WorkflowFolderUnionFull } from '@/databases/repositories/workflow.repository';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
@ -58,13 +59,33 @@ export class WorkflowService {
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) {
|
async getMany(
|
||||||
|
user: User,
|
||||||
|
options?: ListQuery.Options,
|
||||||
|
includeScopes?: boolean,
|
||||||
|
includeFolders?: boolean,
|
||||||
|
) {
|
||||||
|
let count;
|
||||||
|
let workflows;
|
||||||
|
let workflowsAndFolders: WorkflowFolderUnionFull[] = [];
|
||||||
|
|
||||||
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
|
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
|
||||||
scopes: ['workflow:read'],
|
scopes: ['workflow:read'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
if (includeFolders) {
|
||||||
let { workflows, count } = await this.workflowRepository.getMany(sharedWorkflowIds, options);
|
[workflowsAndFolders, count] = await this.workflowRepository.getWorkflowsAndFoldersWithCount(
|
||||||
|
sharedWorkflowIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
workflows = workflowsAndFolders.filter((wf) => wf.resource === 'workflow');
|
||||||
|
} else {
|
||||||
|
({ workflows, count } = await this.workflowRepository.getManyAndCount(
|
||||||
|
sharedWorkflowIds,
|
||||||
|
options,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Since we're filtering using project ID as part of the relation,
|
Since we're filtering using project ID as part of the relation,
|
||||||
|
@ -83,6 +104,10 @@ export class WorkflowService {
|
||||||
|
|
||||||
this.cleanupSharedField(workflows);
|
this.cleanupSharedField(workflows);
|
||||||
|
|
||||||
|
if (includeFolders) {
|
||||||
|
workflows = this.mergeProcessedWorkflows(workflowsAndFolders, workflows);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflows,
|
workflows,
|
||||||
count,
|
count,
|
||||||
|
@ -137,6 +162,17 @@ export class WorkflowService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mergeProcessedWorkflows(
|
||||||
|
workflowsAndFolders: WorkflowFolderUnionFull[],
|
||||||
|
processedWorkflows: ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
|
||||||
|
) {
|
||||||
|
const workflowMap = new Map(processedWorkflows.map((workflow) => [workflow.id, workflow]));
|
||||||
|
|
||||||
|
return workflowsAndFolders.map((item) =>
|
||||||
|
item.resource === 'workflow' ? (workflowMap.get(item.id) ?? item) : item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
async update(
|
async update(
|
||||||
user: User,
|
user: User,
|
||||||
|
|
|
@ -212,6 +212,7 @@ export class WorkflowsController {
|
||||||
req.user,
|
req.user,
|
||||||
req.listQueryOptions,
|
req.listQueryOptions,
|
||||||
!!req.query.includeScopes,
|
!!req.query.includeScopes,
|
||||||
|
!!req.query.includeFolders,
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ count, data });
|
res.json({ count, data });
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const createFolder = async (
|
||||||
const folder = await folderRepository.save(
|
const folder = await folderRepository.save(
|
||||||
folderRepository.create({
|
folderRepository.create({
|
||||||
name: options.name ?? randomName(),
|
name: options.name ?? randomName(),
|
||||||
project,
|
homeProject: project,
|
||||||
parentFolder: options.parentFolder ?? null,
|
parentFolder: options.parentFolder ?? null,
|
||||||
tags: options.tags ?? [],
|
tags: options.tags ?? [],
|
||||||
updatedAt: options.updatedAt ?? new Date(),
|
updatedAt: options.updatedAt ?? new Date(),
|
||||||
|
|
|
@ -9,11 +9,13 @@ import type { User } from '@/databases/entities/user';
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||||
import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository';
|
import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository';
|
||||||
|
import type { WorkflowFolderUnionFull } from '@/databases/repositories/workflow.repository';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
import { ProjectService } from '@/services/project.service.ee';
|
import { ProjectService } from '@/services/project.service.ee';
|
||||||
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
||||||
|
import { createFolder } from '@test-integration/db/folders';
|
||||||
|
|
||||||
import { mockInstance } from '../../shared/mocking';
|
import { mockInstance } from '../../shared/mocking';
|
||||||
import { saveCredential } from '../shared/db/credentials';
|
import { saveCredential } from '../shared/db/credentials';
|
||||||
|
@ -62,6 +64,7 @@ beforeEach(async () => {
|
||||||
'WorkflowHistory',
|
'WorkflowHistory',
|
||||||
'Project',
|
'Project',
|
||||||
'ProjectRelation',
|
'ProjectRelation',
|
||||||
|
'Folder',
|
||||||
]);
|
]);
|
||||||
projectRepository = Container.get(ProjectRepository);
|
projectRepository = Container.get(ProjectRepository);
|
||||||
projectService = Container.get(ProjectService);
|
projectService = Container.get(ProjectService);
|
||||||
|
@ -723,6 +726,33 @@ describe('GET /workflows', () => {
|
||||||
expect(response2.body.data).toHaveLength(0);
|
expect(response2.body.data).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should filter workflows by parentFolderId', async () => {
|
||||||
|
const pp = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(owner.id);
|
||||||
|
|
||||||
|
const folder1 = await createFolder(pp, { name: 'Folder 1' });
|
||||||
|
|
||||||
|
const workflow1 = await createWorkflow({ name: 'First', parentFolder: folder1 }, owner);
|
||||||
|
|
||||||
|
const workflow2 = await createWorkflow({ name: 'Second' }, owner);
|
||||||
|
|
||||||
|
const response1 = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query(`filter={ "parentFolderId": "${folder1.id}" }`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response1.body.data).toHaveLength(1);
|
||||||
|
expect(response1.body.data[0].id).toBe(workflow1.id);
|
||||||
|
|
||||||
|
// if not provided, looks for workflows without a parentFolder
|
||||||
|
const response2 = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('filter={ "parentFolderId": "0" }');
|
||||||
|
expect(200);
|
||||||
|
|
||||||
|
expect(response2.body.data).toHaveLength(1);
|
||||||
|
expect(response2.body.data[0].id).toBe(workflow2.id);
|
||||||
|
});
|
||||||
|
|
||||||
test('should return homeProject when filtering workflows by projectId', async () => {
|
test('should return homeProject when filtering workflows by projectId', async () => {
|
||||||
const workflow = await createWorkflow({ name: 'First' }, owner);
|
const workflow = await createWorkflow({ name: 'First' }, owner);
|
||||||
await shareWorkflowWithUsers(workflow, [member]);
|
await shareWorkflowWithUsers(workflow, [member]);
|
||||||
|
@ -893,6 +923,40 @@ describe('GET /workflows', () => {
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should select workflow field: parentFolder', async () => {
|
||||||
|
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
|
||||||
|
owner.id,
|
||||||
|
);
|
||||||
|
const folder = await createFolder(ownerPersonalProject, { name: 'Folder 1' });
|
||||||
|
|
||||||
|
await createWorkflow({ parentFolder: folder }, owner);
|
||||||
|
await createWorkflow({}, owner);
|
||||||
|
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('select=["parentFolder"]')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 2,
|
||||||
|
data: arrayContaining([
|
||||||
|
{
|
||||||
|
id: any(String),
|
||||||
|
parentFolder: {
|
||||||
|
id: folder.id,
|
||||||
|
name: folder.name,
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: any(String),
|
||||||
|
parentFolder: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sortBy', () => {
|
describe('sortBy', () => {
|
||||||
|
@ -1056,6 +1120,647 @@ describe('GET /workflows', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /workflows?includeFolders=true', () => {
|
||||||
|
test('should return zero workflows and folders if none exist', async () => {
|
||||||
|
const response = await authOwnerAgent.get('/workflows').query({ includeFolders: true });
|
||||||
|
|
||||||
|
expect(response.body).toEqual({ count: 0, data: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return workflows and folders', async () => {
|
||||||
|
const credential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: owner,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||||
|
|
||||||
|
const nodes: INode[] = [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: 'Action Network',
|
||||||
|
type: 'n8n-nodes-base.actionNetwork',
|
||||||
|
parameters: {},
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0],
|
||||||
|
credentials: {
|
||||||
|
actionNetworkApi: {
|
||||||
|
id: credential.id,
|
||||||
|
name: credential.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tag = await createTag({ name: 'A' });
|
||||||
|
|
||||||
|
await createWorkflow({ name: 'First', nodes, tags: [tag] }, owner);
|
||||||
|
await createWorkflow({ name: 'Second' }, owner);
|
||||||
|
await createFolder(ownerPersonalProject, { name: 'Folder' });
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.get('/workflows').query({ includeFolders: true });
|
||||||
|
expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 3,
|
||||||
|
data: arrayContaining([
|
||||||
|
objectContaining({
|
||||||
|
resource: 'workflow',
|
||||||
|
id: any(String),
|
||||||
|
name: 'First',
|
||||||
|
active: any(Boolean),
|
||||||
|
createdAt: any(String),
|
||||||
|
updatedAt: any(String),
|
||||||
|
tags: [{ id: any(String), name: 'A' }],
|
||||||
|
versionId: any(String),
|
||||||
|
homeProject: {
|
||||||
|
id: ownerPersonalProject.id,
|
||||||
|
name: owner.createPersonalProjectName(),
|
||||||
|
icon: null,
|
||||||
|
type: ownerPersonalProject.type,
|
||||||
|
},
|
||||||
|
sharedWithProjects: [],
|
||||||
|
}),
|
||||||
|
objectContaining({
|
||||||
|
id: any(String),
|
||||||
|
name: 'Second',
|
||||||
|
active: any(Boolean),
|
||||||
|
createdAt: any(String),
|
||||||
|
updatedAt: any(String),
|
||||||
|
tags: [],
|
||||||
|
versionId: any(String),
|
||||||
|
homeProject: {
|
||||||
|
id: ownerPersonalProject.id,
|
||||||
|
name: owner.createPersonalProjectName(),
|
||||||
|
icon: null,
|
||||||
|
type: ownerPersonalProject.type,
|
||||||
|
},
|
||||||
|
sharedWithProjects: [],
|
||||||
|
}),
|
||||||
|
objectContaining({
|
||||||
|
resource: 'folder',
|
||||||
|
id: any(String),
|
||||||
|
name: 'Folder',
|
||||||
|
createdAt: any(String),
|
||||||
|
updatedAt: any(String),
|
||||||
|
tags: [],
|
||||||
|
homeProject: {
|
||||||
|
id: ownerPersonalProject.id,
|
||||||
|
name: owner.createPersonalProjectName(),
|
||||||
|
icon: null,
|
||||||
|
type: ownerPersonalProject.type,
|
||||||
|
},
|
||||||
|
parentFolder: null,
|
||||||
|
workflowsCount: 0,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const found = response.body.data.find(
|
||||||
|
(w: ListQuery.Workflow.WithOwnership) => w.name === 'First',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(found.nodes).toBeUndefined();
|
||||||
|
expect(found.sharedWithProjects).toHaveLength(0);
|
||||||
|
expect(found.usedCredentials).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return workflows with scopes and folders when ?includeScopes=true', async () => {
|
||||||
|
const [member1, member2] = await createManyUsers(2, {
|
||||||
|
role: 'global:member',
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamProject = await createTeamProject(undefined, member1);
|
||||||
|
await linkUserToProject(member2, teamProject, 'project:editor');
|
||||||
|
|
||||||
|
const credential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
user: owner,
|
||||||
|
role: 'credential:owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes: INode[] = [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: 'Action Network',
|
||||||
|
type: 'n8n-nodes-base.actionNetwork',
|
||||||
|
parameters: {},
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0],
|
||||||
|
credentials: {
|
||||||
|
actionNetworkApi: {
|
||||||
|
id: credential.id,
|
||||||
|
name: credential.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tag = await createTag({ name: 'A' });
|
||||||
|
|
||||||
|
const [savedWorkflow1, savedWorkflow2, savedFolder1] = await Promise.all([
|
||||||
|
createWorkflow({ name: 'First', nodes, tags: [tag] }, teamProject),
|
||||||
|
createWorkflow({ name: 'Second' }, member2),
|
||||||
|
createFolder(teamProject, { name: 'Folder' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await shareWorkflowWithProjects(savedWorkflow2, [{ project: teamProject }]);
|
||||||
|
|
||||||
|
{
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(member1)
|
||||||
|
.get('/workflows?includeScopes=true&includeFolders=true');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
|
||||||
|
const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
|
||||||
|
const wf1 = workflows.find((wf) => wf.id === savedWorkflow1.id)!;
|
||||||
|
const wf2 = workflows.find((wf) => wf.id === savedWorkflow2.id)!;
|
||||||
|
const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;
|
||||||
|
|
||||||
|
// Team workflow
|
||||||
|
expect(wf1.id).toBe(savedWorkflow1.id);
|
||||||
|
expect(wf1.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:execute',
|
||||||
|
'workflow:move',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:update',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shared workflow
|
||||||
|
expect(wf2.id).toBe(savedWorkflow2.id);
|
||||||
|
expect(wf2.scopes).toEqual(['workflow:read', 'workflow:update', 'workflow:execute'].sort());
|
||||||
|
|
||||||
|
expect(f1.id).toBe(savedFolder1.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(member2)
|
||||||
|
.get('/workflows?includeScopes=true&includeFolders=true');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
|
||||||
|
const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
|
||||||
|
const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
|
||||||
|
const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;
|
||||||
|
const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;
|
||||||
|
|
||||||
|
// Team workflow
|
||||||
|
expect(wf1.id).toBe(savedWorkflow1.id);
|
||||||
|
expect(wf1.scopes).toEqual([
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:execute',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:update',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Shared workflow
|
||||||
|
expect(wf2.id).toBe(savedWorkflow2.id);
|
||||||
|
expect(wf2.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:execute',
|
||||||
|
'workflow:move',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:share',
|
||||||
|
'workflow:update',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(f1.id).toBe(savedFolder1.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(owner)
|
||||||
|
.get('/workflows?includeScopes=true&includeFolders=true');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
|
||||||
|
const workflows = response.body.data as Array<WorkflowFolderUnionFull & { scopes: Scope[] }>;
|
||||||
|
const wf1 = workflows.find((w) => w.id === savedWorkflow1.id)!;
|
||||||
|
const wf2 = workflows.find((w) => w.id === savedWorkflow2.id)!;
|
||||||
|
const f1 = workflows.find((wf) => wf.id === savedFolder1.id)!;
|
||||||
|
|
||||||
|
// Team workflow
|
||||||
|
expect(wf1.id).toBe(savedWorkflow1.id);
|
||||||
|
expect(wf1.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'workflow:create',
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:execute',
|
||||||
|
'workflow:list',
|
||||||
|
'workflow:move',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:share',
|
||||||
|
'workflow:update',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shared workflow
|
||||||
|
expect(wf2.id).toBe(savedWorkflow2.id);
|
||||||
|
expect(wf2.scopes).toEqual(
|
||||||
|
[
|
||||||
|
'workflow:create',
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:execute',
|
||||||
|
'workflow:list',
|
||||||
|
'workflow:move',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:share',
|
||||||
|
'workflow:update',
|
||||||
|
].sort(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(f1.id).toBe(savedFolder1.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter', () => {
|
||||||
|
test('should filter workflows and folders by field: name', async () => {
|
||||||
|
const workflow1 = await createWorkflow({ name: 'First' }, owner);
|
||||||
|
await createWorkflow({ name: 'Second' }, owner);
|
||||||
|
|
||||||
|
const ownerProject = await getPersonalProject(owner);
|
||||||
|
|
||||||
|
const folder1 = await createFolder(ownerProject, { name: 'First' });
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('filter={"name":"First"}&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 2,
|
||||||
|
data: [
|
||||||
|
objectContaining({ id: folder1.id, name: 'First' }),
|
||||||
|
objectContaining({ id: workflow1.id, name: 'First' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should filter workflows and folders by field: active', async () => {
|
||||||
|
const workflow1 = await createWorkflow({ active: true }, owner);
|
||||||
|
await createWorkflow({ active: false }, owner);
|
||||||
|
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('filter={ "active": true }&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 1,
|
||||||
|
data: [objectContaining({ id: workflow1.id, active: true })],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should filter workflows and folders by field: tags (AND operator)', async () => {
|
||||||
|
const baseDate = DateTime.now();
|
||||||
|
|
||||||
|
const workflow1 = await createWorkflow(
|
||||||
|
{ name: 'First', updatedAt: baseDate.toJSDate() },
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
const workflow2 = await createWorkflow(
|
||||||
|
{ name: 'Second', updatedAt: baseDate.toJSDate() },
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ownerProject = await getPersonalProject(owner);
|
||||||
|
|
||||||
|
const tagA = await createTag(
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
},
|
||||||
|
workflow1,
|
||||||
|
);
|
||||||
|
const tagB = await createTag(
|
||||||
|
{
|
||||||
|
name: 'B',
|
||||||
|
},
|
||||||
|
workflow1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await createTag({ name: 'C' }, workflow2);
|
||||||
|
|
||||||
|
await createFolder(ownerProject, {
|
||||||
|
name: 'First Folder',
|
||||||
|
tags: [tagA, tagB],
|
||||||
|
updatedAt: baseDate.plus({ minutes: 2 }).toJSDate(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('filter={ "tags": ["A", "B"] }&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 2,
|
||||||
|
data: [
|
||||||
|
objectContaining({
|
||||||
|
name: 'First Folder',
|
||||||
|
tags: expect.arrayContaining([
|
||||||
|
{ id: any(String), name: 'A' },
|
||||||
|
{ id: any(String), name: 'B' },
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
objectContaining({
|
||||||
|
name: 'First',
|
||||||
|
tags: expect.arrayContaining([
|
||||||
|
{ id: any(String), name: 'A' },
|
||||||
|
{ id: any(String), name: 'B' },
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should filter workflows by projectId', async () => {
|
||||||
|
const workflow = await createWorkflow({ name: 'First' }, owner);
|
||||||
|
const pp = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(owner.id);
|
||||||
|
|
||||||
|
const folder = await createFolder(pp, {
|
||||||
|
name: 'First Folder',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response1 = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query(`filter={ "projectId": "${pp.id}" }&includeFolders=true`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response1.body.data).toHaveLength(2);
|
||||||
|
expect(response1.body.data[0].id).toBe(folder.id);
|
||||||
|
expect(response1.body.data[1].id).toBe(workflow.id);
|
||||||
|
|
||||||
|
const response2 = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('filter={ "projectId": "Non-Existing Project ID" }&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response2.body.data).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return homeProject when filtering workflows and folders by projectId', async () => {
|
||||||
|
const workflow = await createWorkflow({ name: 'First' }, owner);
|
||||||
|
await shareWorkflowWithUsers(workflow, [member]);
|
||||||
|
const pp = await getPersonalProject(member);
|
||||||
|
const folder = await createFolder(pp, {
|
||||||
|
name: 'First Folder',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authMemberAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query(`filter={ "projectId": "${pp.id}" }&includeFolders=true`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(2);
|
||||||
|
expect(response.body.data[0].id).toBe(folder.id);
|
||||||
|
expect(response.body.data[0].homeProject).not.toBeNull();
|
||||||
|
expect(response.body.data[1].id).toBe(workflow.id);
|
||||||
|
expect(response.body.data[1].homeProject).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sortBy', () => {
|
||||||
|
test('should fail when trying to sort by non sortable column', async () => {
|
||||||
|
await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=nonSortableColumn:asc&?includeFolders=true')
|
||||||
|
.expect(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should sort by createdAt column', async () => {
|
||||||
|
await createWorkflow({ name: 'First' }, owner);
|
||||||
|
await createWorkflow({ name: 'Second' }, owner);
|
||||||
|
const pp = await getPersonalProject(owner);
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'First Folder',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'Z Folder',
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=createdAt:asc&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 4,
|
||||||
|
data: arrayContaining([
|
||||||
|
expect.objectContaining({ name: 'First Folder' }),
|
||||||
|
expect.objectContaining({ name: 'Z Folder' }),
|
||||||
|
expect.objectContaining({ name: 'First' }),
|
||||||
|
expect.objectContaining({ name: 'Second' }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=createdAt:asc&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 4,
|
||||||
|
data: arrayContaining([
|
||||||
|
expect.objectContaining({ name: 'Z Folder' }),
|
||||||
|
expect.objectContaining({ name: 'First Folder' }),
|
||||||
|
expect.objectContaining({ name: 'Second' }),
|
||||||
|
expect.objectContaining({ name: 'First' }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should sort by name column', async () => {
|
||||||
|
await createWorkflow({ name: 'a' }, owner);
|
||||||
|
await createWorkflow({ name: 'b' }, owner);
|
||||||
|
await createWorkflow({ name: 'My workflow' }, owner);
|
||||||
|
const pp = await getPersonalProject(owner);
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'a Folder',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'Z Folder',
|
||||||
|
});
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=name:asc&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 5,
|
||||||
|
data: [
|
||||||
|
expect.objectContaining({ name: 'a Folder' }),
|
||||||
|
expect.objectContaining({ name: 'Z Folder' }),
|
||||||
|
expect.objectContaining({ name: 'a' }),
|
||||||
|
expect.objectContaining({ name: 'b' }),
|
||||||
|
expect.objectContaining({ name: 'My workflow' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=name:desc&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 5,
|
||||||
|
data: [
|
||||||
|
expect.objectContaining({ name: 'Z Folder' }),
|
||||||
|
expect.objectContaining({ name: 'a Folder' }),
|
||||||
|
expect.objectContaining({ name: 'My workflow' }),
|
||||||
|
expect.objectContaining({ name: 'b' }),
|
||||||
|
expect.objectContaining({ name: 'a' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should sort by updatedAt column', async () => {
|
||||||
|
const baseDate = DateTime.now();
|
||||||
|
|
||||||
|
const pp = await getPersonalProject(owner);
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'Folder',
|
||||||
|
});
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'Z Folder',
|
||||||
|
});
|
||||||
|
await createWorkflow(
|
||||||
|
{ name: 'Second', updatedAt: baseDate.plus({ minutes: 1 }).toJSDate() },
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
await createWorkflow(
|
||||||
|
{ name: 'First', updatedAt: baseDate.plus({ minutes: 2 }).toJSDate() },
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=updatedAt:asc&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 4,
|
||||||
|
data: arrayContaining([
|
||||||
|
expect.objectContaining({ name: 'Folder' }),
|
||||||
|
expect.objectContaining({ name: 'Z Folder' }),
|
||||||
|
expect.objectContaining({ name: 'Second' }),
|
||||||
|
expect.objectContaining({ name: 'First' }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('sortBy=updatedAt:desc&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 4,
|
||||||
|
data: arrayContaining([
|
||||||
|
expect.objectContaining({ name: 'Z Folder' }),
|
||||||
|
expect.objectContaining({ name: 'Folder' }),
|
||||||
|
expect.objectContaining({ name: 'First' }),
|
||||||
|
expect.objectContaining({ name: 'Second' }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pagination', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const pp = await getPersonalProject(owner);
|
||||||
|
await createWorkflow({ name: 'Workflow 1' }, owner);
|
||||||
|
await createWorkflow({ name: 'Workflow 2' }, owner);
|
||||||
|
await createWorkflow({ name: 'Workflow 3' }, owner);
|
||||||
|
await createWorkflow({ name: 'Workflow 4' }, owner);
|
||||||
|
await createWorkflow({ name: 'Workflow 5' }, owner);
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'Folder 1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail when skip is provided without take', async () => {
|
||||||
|
await authOwnerAgent.get('/workflows?includeFolders=true').query('skip=2').expect(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle skip with take parameter', async () => {
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('skip=2&take=4&includeFolders=true');
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(4);
|
||||||
|
expect(response.body.count).toBe(6);
|
||||||
|
expect(response.body.data[0].name).toBe('Workflow 2');
|
||||||
|
expect(response.body.data[1].name).toBe('Workflow 3');
|
||||||
|
expect(response.body.data[2].name).toBe('Workflow 4');
|
||||||
|
expect(response.body.data[3].name).toBe('Workflow 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle pagination with sorting', async () => {
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('skip=1&take=2&sortBy=name:desc&includeFolders=true');
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(2);
|
||||||
|
expect(response.body.count).toBe(6);
|
||||||
|
expect(response.body.data[0].name).toBe('Workflow 5');
|
||||||
|
expect(response.body.data[1].name).toBe('Workflow 4');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle pagination with filtering', async () => {
|
||||||
|
const pp = await getPersonalProject(owner);
|
||||||
|
await createWorkflow({ name: 'Special Workflow 1' }, owner);
|
||||||
|
await createWorkflow({ name: 'Special Workflow 2' }, owner);
|
||||||
|
await createWorkflow({ name: 'Special Workflow 3' }, owner);
|
||||||
|
await createFolder(pp, {
|
||||||
|
name: 'Special Folder 1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('take=2&skip=1')
|
||||||
|
.query('filter={"name":"Special"}&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(2);
|
||||||
|
expect(response.body.count).toBe(4);
|
||||||
|
expect(response.body.data[0].name).toBe('Special Workflow 1');
|
||||||
|
expect(response.body.data[1].name).toBe('Special Workflow 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty array when pagination exceeds total count', async () => {
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('take=2&skip=10&includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(0);
|
||||||
|
expect(response.body.count).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return all results when no pagination parameters are provided', async () => {
|
||||||
|
const response = await authOwnerAgent
|
||||||
|
.get('/workflows')
|
||||||
|
.query('includeFolders=true')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveLength(6);
|
||||||
|
expect(response.body.count).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('PATCH /workflows/:workflowId', () => {
|
describe('PATCH /workflows/:workflowId', () => {
|
||||||
test('should create workflow history version when licensed', async () => {
|
test('should create workflow history version when licensed', async () => {
|
||||||
license.enable('feat:workflowHistory');
|
license.enable('feat:workflowHistory');
|
||||||
|
|
Loading…
Reference in a new issue