mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Update POST /workflows
to link folder (no-changelog) (#13449)
This commit is contained in:
parent
2f395fe89a
commit
461e39a74e
|
@ -1,5 +1,5 @@
|
|||
import { Service } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import type { EntityManager, SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { DataSource, Repository } from '@n8n/typeorm';
|
||||
|
||||
import type { ListQuery } from '@/requests';
|
||||
|
@ -227,8 +227,13 @@ export class FolderRepository extends Repository<FolderWithWorkflowsCount> {
|
|||
}
|
||||
}
|
||||
|
||||
async findOneOrFailFolderInProject(folderId: string, projectId: string): Promise<Folder> {
|
||||
return await this.manager.findOneOrFail(Folder, {
|
||||
async findOneOrFailFolderInProject(
|
||||
folderId: string,
|
||||
projectId: string,
|
||||
em?: EntityManager,
|
||||
): Promise<Folder> {
|
||||
const manager = em ?? this.manager;
|
||||
return await manager.findOneOrFail(Folder, {
|
||||
where: {
|
||||
id: folderId,
|
||||
homeProject: {
|
||||
|
|
|
@ -112,7 +112,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
|||
workflowId: string,
|
||||
user: User,
|
||||
scopes: Scope[],
|
||||
{ includeTags = false, em = this.manager } = {},
|
||||
{ includeTags = false, includeParentFolder = false, em = this.manager } = {},
|
||||
) {
|
||||
let where: FindOptionsWhere<SharedWorkflow> = { workflowId };
|
||||
|
||||
|
@ -138,6 +138,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
|||
workflow: {
|
||||
shared: { project: { projectRelations: { user: true } } },
|
||||
tags: includeTags,
|
||||
parentFolder: includeParentFolder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import type { CreateFolderDto } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
|
||||
import { FolderRepository } from '@/databases/repositories/folder.repository';
|
||||
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||
|
@ -37,9 +39,9 @@ export class FolderService {
|
|||
return folder;
|
||||
}
|
||||
|
||||
async getFolderInProject(folderId: string, projectId: string) {
|
||||
async getFolderInProject(folderId: string, projectId: string, em?: EntityManager) {
|
||||
try {
|
||||
return await this.folderRepository.findOneOrFailFolderInProject(folderId, projectId);
|
||||
return await this.folderRepository.findOneOrFailFolderInProject(folderId, projectId, em);
|
||||
} catch {
|
||||
throw new FolderNotFoundError(folderId);
|
||||
}
|
||||
|
@ -48,6 +50,10 @@ export class FolderService {
|
|||
async getFolderTree(folderId: string, projectId: string): Promise<SimpleFolderNode[]> {
|
||||
await this.getFolderInProject(folderId, projectId);
|
||||
|
||||
const escapedParentFolderId = this.folderRepository
|
||||
.createQueryBuilder()
|
||||
.escape('parentFolderId');
|
||||
|
||||
const baseQuery = this.folderRepository
|
||||
.createQueryBuilder('folder')
|
||||
.select('folder.id', 'id')
|
||||
|
@ -58,7 +64,7 @@ export class FolderService {
|
|||
.createQueryBuilder('f')
|
||||
.select('f.id', 'id')
|
||||
.addSelect('f.parentFolderId', 'parentFolderId')
|
||||
.innerJoin('folder_path', 'fp', 'f.id = fp.parentFolderId');
|
||||
.innerJoin('folder_path', 'fp', `f.id = fp.${escapedParentFolderId}`);
|
||||
|
||||
const mainQuery = this.folderRepository
|
||||
.createQueryBuilder('folder')
|
||||
|
|
|
@ -22,6 +22,7 @@ export declare namespace WorkflowRequest {
|
|||
hash: string;
|
||||
meta: Record<string, unknown>;
|
||||
projectId: string;
|
||||
parentFolderId?: string;
|
||||
}>;
|
||||
|
||||
type ManualRunPayload = {
|
||||
|
|
|
@ -46,6 +46,7 @@ import { License } from '@/license';
|
|||
import { listQueryMiddleware } from '@/middlewares';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import * as ResponseHelper from '@/response-helper';
|
||||
import { FolderService } from '@/services/folder.service';
|
||||
import { NamingService } from '@/services/naming.service';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
import { TagService } from '@/services/tag.service';
|
||||
|
@ -82,6 +83,7 @@ export class WorkflowsController {
|
|||
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||
private readonly eventService: EventService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly folderService: FolderService,
|
||||
) {}
|
||||
|
||||
@Post('/')
|
||||
|
@ -133,7 +135,7 @@ export class WorkflowsController {
|
|||
const savedWorkflow = await Db.transaction(async (transactionManager) => {
|
||||
const workflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
|
||||
const { projectId } = req.body;
|
||||
const { projectId, parentFolderId } = req.body;
|
||||
project =
|
||||
projectId === undefined
|
||||
? await this.projectRepository.getPersonalProjectForUser(req.user.id, transactionManager)
|
||||
|
@ -155,6 +157,17 @@ export class WorkflowsController {
|
|||
throw new ApplicationError('No personal project found');
|
||||
}
|
||||
|
||||
if (parentFolderId) {
|
||||
try {
|
||||
const parentFolder = await this.folderService.getFolderInProject(
|
||||
parentFolderId,
|
||||
project.id,
|
||||
transactionManager,
|
||||
);
|
||||
await transactionManager.update(WorkflowEntity, { id: workflow.id }, { parentFolder });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const newSharedWorkflow = this.sharedWorkflowRepository.create({
|
||||
role: 'workflow:owner',
|
||||
projectId: project.id,
|
||||
|
@ -167,7 +180,7 @@ export class WorkflowsController {
|
|||
workflow.id,
|
||||
req.user,
|
||||
['workflow:read'],
|
||||
{ em: transactionManager, includeTags: true },
|
||||
{ em: transactionManager, includeTags: true, includeParentFolder: true },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -58,13 +58,14 @@ let projectService: ProjectService;
|
|||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate([
|
||||
'Workflow',
|
||||
'SharedWorkflow',
|
||||
'Tag',
|
||||
'WorkflowHistory',
|
||||
'Project',
|
||||
'ProjectRelation',
|
||||
'Folder',
|
||||
'Workflow',
|
||||
'Tag',
|
||||
'Project',
|
||||
'User',
|
||||
]);
|
||||
projectRepository = Container.get(ProjectRepository);
|
||||
projectService = Container.get(ProjectService);
|
||||
|
@ -378,6 +379,115 @@ describe('POST /workflows', () => {
|
|||
message: "You don't have the permissions to save the workflow in this project.",
|
||||
});
|
||||
});
|
||||
|
||||
test('create link workflow with folder if one is provided', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const folder = await createFolder(personalProject, { name: 'Folder 1' });
|
||||
|
||||
const workflow = makeWorkflow();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await authOwnerAgent
|
||||
.post('/workflows')
|
||||
.send({ ...workflow, parentFolderId: folder.id });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
|
||||
expect(response.body.data).toMatchObject({
|
||||
active: false,
|
||||
id: expect.any(String),
|
||||
name: workflow.name,
|
||||
sharedWithProjects: [],
|
||||
usedCredentials: [],
|
||||
homeProject: {
|
||||
id: personalProject.id,
|
||||
name: personalProject.name,
|
||||
type: personalProject.type,
|
||||
},
|
||||
parentFolder: {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
},
|
||||
});
|
||||
expect(response.body.data.shared).toBeUndefined();
|
||||
});
|
||||
|
||||
test('create workflow without parent folder if no folder is provided', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const workflow = makeWorkflow();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await authOwnerAgent
|
||||
.post('/workflows')
|
||||
.send({ ...workflow })
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
|
||||
expect(response.body.data).toMatchObject({
|
||||
active: false,
|
||||
id: expect.any(String),
|
||||
name: workflow.name,
|
||||
sharedWithProjects: [],
|
||||
usedCredentials: [],
|
||||
homeProject: {
|
||||
id: personalProject.id,
|
||||
name: personalProject.name,
|
||||
type: personalProject.type,
|
||||
},
|
||||
parentFolder: null,
|
||||
});
|
||||
expect(response.body.data.shared).toBeUndefined();
|
||||
});
|
||||
|
||||
test('create workflow without parent is provided folder does not exist in the project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const workflow = makeWorkflow();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await authOwnerAgent
|
||||
.post('/workflows')
|
||||
.send({ ...workflow, parentFolderId: 'non-existing-folder-id' })
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
|
||||
expect(response.body.data).toMatchObject({
|
||||
active: false,
|
||||
id: expect.any(String),
|
||||
name: workflow.name,
|
||||
sharedWithProjects: [],
|
||||
usedCredentials: [],
|
||||
homeProject: {
|
||||
id: personalProject.id,
|
||||
name: personalProject.name,
|
||||
type: personalProject.type,
|
||||
},
|
||||
parentFolder: null,
|
||||
});
|
||||
expect(response.body.data.shared).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /workflows/:workflowId', () => {
|
||||
|
|
Loading…
Reference in a new issue