From 707dad2ec0f44aecafa76a191ac2c2097d8592ae Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Fri, 16 Feb 2024 15:30:19 +0100 Subject: [PATCH 1/8] allow creating workflows in other projects than the personal one --- packages/cli/src/services/project.service.ts | 34 ++- .../cli/src/workflows/workflow.request.ts | 3 +- .../cli/src/workflows/workflows.controller.ts | 27 ++- .../services/project.service.test.ts | 196 ++++++++++++++++++ .../workflows/workflows.controller.test.ts | 117 +++++++++++ 5 files changed, 368 insertions(+), 9 deletions(-) create mode 100644 packages/cli/test/integration/services/project.service.test.ts diff --git a/packages/cli/src/services/project.service.ts b/packages/cli/src/services/project.service.ts index 3e3884ab51880..87b43c3ef90e2 100644 --- a/packages/cli/src/services/project.service.ts +++ b/packages/cli/src/services/project.service.ts @@ -1,16 +1,22 @@ import type { Project } from '@/databases/entities/Project'; -import { ProjectRelation, type ProjectRole } from '@/databases/entities/ProjectRelation'; +import { ProjectRelation } from '@/databases/entities/ProjectRelation'; +import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { User } from '@/databases/entities/User'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { Not, type EntityManager } from '@n8n/typeorm'; import { Service } from 'typedi'; +import { type Scope } from '@n8n/permissions'; +import { In } from '@n8n/typeorm'; +import { RoleService } from './role.service'; @Service() export class ProjectService { constructor( - private projectsRepository: ProjectRepository, - private projectRelationRepository: ProjectRelationRepository, + private readonly projectsRepository: ProjectRepository, + private readonly projectRepository: ProjectRepository, + private readonly projectRelationRepository: ProjectRelationRepository, + private readonly roleService: RoleService, ) {} async getAccessibleProjects(user: User): Promise { @@ -110,4 +116,26 @@ export class ProjectService { ), ); } + + async getProjectWithScope(user: User, projectId: string, scope: Scope) { + const projectRoles = this.roleService.rolesWithScope('project', [scope]); + + return await this.projectRepository.findOne({ + where: { + id: projectId, + projectRelations: { + role: In(projectRoles), + userId: user.id, + }, + }, + }); + } + + async addUser(projectId: string, userId: string, role: ProjectRole) { + return await this.projectRelationRepository.save({ + projectId, + userId, + role, + }); + } } diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index 773f12950260a..faffc6e382352 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -4,7 +4,7 @@ import type { INode, IConnections, IWorkflowSettings, IRunData, IPinData } from export declare namespace WorkflowRequest { type CreateUpdatePayload = Partial<{ - id: string; // delete if sent + id: string; // deleted if sent name: string; nodes: INode[]; connections: IConnections; @@ -13,6 +13,7 @@ export declare namespace WorkflowRequest { tags: string[]; hash: string; meta: Record; + projectId: string; }>; type ManualRunPayload = { diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index a2df2fa30cd3f..d4dc5e432af29 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -18,7 +18,7 @@ import { Put, RestController, } from '@/decorators'; -import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow'; +import type { SharedWorkflow, WorkflowSharingRole } from '@db/entities/SharedWorkflow'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { TagRepository } from '@db/repositories/tag.repository'; @@ -48,6 +48,7 @@ import { WorkflowExecutionService } from './workflowExecution.service'; import { WorkflowSharingService } from './workflowSharing.service'; import { UserManagementMailer } from '@/UserManagement/email'; import { ProjectRepository } from '@/databases/repositories/project.repository'; +import { ProjectService } from '@/services/project.service'; @Authorized() @RestController('/workflows') @@ -72,6 +73,7 @@ export class WorkflowsController { private readonly mailer: UserManagementMailer, private readonly credentialsService: CredentialsService, private readonly projectRepository: ProjectRepository, + private readonly projectService: ProjectService, ) {} @Post('/') @@ -121,14 +123,29 @@ export class WorkflowsController { await Db.transaction(async (transactionManager) => { savedWorkflow = await transactionManager.save(newWorkflow); - const project = await this.projectRepository.getPersonalProjectForUserOrFail(req.user.id); + const { projectId } = req.body; + const project = + projectId === undefined + ? // TODO: pass the transaction manager optionally? + await this.projectRepository.getPersonalProjectForUser(req.user.id) + : await this.projectService.getProjectWithScope(req.user, projectId, 'workflow:create'); - const newSharedWorkflow = new SharedWorkflow(); + if (typeof projectId === 'string' && project === null) { + throw new BadRequestError( + "You don't have the permissions to save the workflow in this project.", + ); + } + + // Safe guard in case the personal project does not exist for whatever reason. + if (project === null) { + throw new InternalServerError('Failed to save workflow'); + } - Object.assign(newSharedWorkflow, { + const newSharedWorkflow = this.sharedWorkflowRepository.create({ role: 'workflow:owner', + // TODO: remove when https://linear.app/n8n/issue/PAY-1353/make-sure-that-sharedworkflowuserid-is-not-used-anymore-to-check lands user: req.user, - project, + projectId: project.id, workflow: savedWorkflow, }); diff --git a/packages/cli/test/integration/services/project.service.test.ts b/packages/cli/test/integration/services/project.service.test.ts new file mode 100644 index 0000000000000..b06012ac40897 --- /dev/null +++ b/packages/cli/test/integration/services/project.service.test.ts @@ -0,0 +1,196 @@ +import { ProjectService } from '@/services/project.service'; +import * as testDb from '../shared/testDb'; +import Container from 'typedi'; +import { createMember } from '../shared/db/users'; +import { ProjectRepository } from '@/databases/repositories/project.repository'; +import type { DeepPartial } from 'ts-essentials'; +import type { Project } from '@/databases/entities/Project'; +import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; +import { EntityNotFoundError } from '@n8n/typeorm'; +import type { ProjectRole } from '@/databases/entities/ProjectRelation'; +import type { Scope } from '@n8n/permissions'; + +let projectRepository: ProjectRepository; +let projectService: ProjectService; +let projectRelationRepository: ProjectRelationRepository; + +beforeAll(async () => { + await testDb.init(); + + projectRepository = Container.get(ProjectRepository); + projectService = Container.get(ProjectService); + projectRelationRepository = Container.get(ProjectRelationRepository); +}); + +afterAll(async () => { + await testDb.terminate(); +}); + +afterEach(async () => { + await testDb.truncate(['User']); +}); + +async function createTeamProject(project?: DeepPartial) { + const projectRepository = Container.get(ProjectRepository); + + let projectEntity = projectRepository.create({ + name: 'Team Project', + ...project, + type: 'team', + }); + + projectEntity = await projectRepository.save(projectEntity); + + return projectEntity; +} + +describe('ProjectService', () => { + describe('addUser', () => { + it.each([ + 'project:viewer', + 'project:admin', + 'project:editor', + 'project:personalOwner', + ] as ProjectRole[])( + 'creates a relation between the user and the project using the role %s', + async (role) => { + // + // ARRANGE + // + const member = await createMember(); + const project = await createTeamProject(); + + // + // ACT + // + await projectService.addUser(project.id, member.id, role); + + // + // ASSERT + // + await projectRelationRepository.findOneOrFail({ + where: { userId: member.id, projectId: project.id, role }, + }); + }, + ); + + it('changes the role the user has to the project instead of creating a new relationship if the user already has a relationship to the project', async () => { + // + // ARRANGE + // + const member = await createMember(); + const project = await createTeamProject(); + await projectService.addUser(project.id, member.id, 'project:viewer'); + + await projectRelationRepository.findOneOrFail({ + where: { userId: member.id, projectId: project.id, role: 'project:viewer' }, + }); + + // + // ACT + // + await projectService.addUser(project.id, member.id, 'project:admin'); + + // + // ASSERT + // + const relationships = await projectRelationRepository.find({ + where: { userId: member.id, projectId: project.id }, + }); + + expect(relationships).toHaveLength(1); + expect(relationships[0]).toHaveProperty('role', 'project:admin'); + }); + }); + + describe('getProjectWithScope', () => { + it.each([ + { role: 'project:admin', scope: 'workflow:list' }, + { role: 'project:admin', scope: 'workflow:create' }, + ] as Array<{ + role: ProjectRole; + scope: Scope; + }>)( + 'should return the project if the user has the role $role and wants the scope $scope', + async ({ role, scope }) => { + // + // ARRANGE + // + const projectOwner = await createMember(); + const project = await createTeamProject(); + await projectService.addUser(project.id, projectOwner.id, role); + + // + // ACT + // + const projectFromService = await projectService.getProjectWithScope( + projectOwner, + project.id, + scope, + ); + + // + // ASSERT + // + if (projectFromService === null) { + fail('Expected projectFromService not to be null'); + } + expect(project.id).toBe(projectFromService.id); + }, + ); + + it.each([ + { role: 'project:viewer', scope: 'workflow:create' }, + { role: 'project:viewer', scope: 'credential:create' }, + ] as Array<{ + role: ProjectRole; + scope: Scope; + }>)( + 'should return the project if the user has the role $role and wants the scope $scope', + async ({ role, scope }) => { + // + // ARRANGE + // + const projectViewer = await createMember(); + const project = await createTeamProject(); + await projectService.addUser(project.id, projectViewer.id, role); + + // + // ACT + // + const projectFromService = await projectService.getProjectWithScope( + projectViewer, + project.id, + scope, + ); + + // + // ASSERT + // + expect(projectFromService).toBeNull(); + }, + ); + + it('should not return the project if the user is not related to it', async () => { + // + // ARRANGE + // + const member = await createMember(); + const project = await createTeamProject(); + + // + // ACT + // + const projectFromService = await projectService.getProjectWithScope( + member, + project.id, + 'workflow:list', + ); + + // + // ASSERT + // + expect(projectFromService).toBeNull(); + }); + }); +}); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index d493dd9d48e1c..d7d8ff8640495 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -21,6 +21,9 @@ import { createOwner } from '../shared/db/users'; import { createWorkflow } from '../shared/db/workflows'; import { createTag } from '../shared/db/tags'; import { License } from '@/License'; +import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; +import { ProjectRepository } from '@/databases/repositories/project.repository'; +import { ProjectService } from '@/services/project.service'; let owner: User; let authOwnerAgent: SuperAgentTest; @@ -151,6 +154,120 @@ describe('POST /workflows', () => { await Container.get(WorkflowHistoryRepository).count({ where: { workflowId: id } }), ).toBe(0); }); + + test('create workflow in personal project by default', async () => { + // + // ARRANGE + // + const projectRepository = Container.get(ProjectRepository); + const workflow = makeWorkflow(); + const personalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); + + // + // ACT + // + const response = await authOwnerAgent.post('/workflows').send(workflow).expect(200); + + // + // ASSERT + // + await Container.get(SharedWorkflowRepository).findOneOrFail({ + where: { + projectId: personalProject.id, + workflowId: response.body.data.id, + }, + }); + }); + + test('creates workflow in other project if projectId is passed', async () => { + // + // ARRANGE + // + const projectRepository = Container.get(ProjectRepository); + const workflow = makeWorkflow(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); + await Container.get(ProjectService).addUser(project.id, owner.id, 'project:admin'); + + // + // ACT + // + const response = await authOwnerAgent + .post('/workflows') + .send({ ...workflow, projectId: project.id }) + .expect(200); + + // + // ASSERT + // + await Container.get(SharedWorkflowRepository).findOneOrFail({ + where: { + projectId: project.id, + workflowId: response.body.data.id, + }, + }); + }); + + test('does not create the workflow in specific project if the project is not shared with user', async () => { + // + // ARRANGE + // + const projectRepository = Container.get(ProjectRepository); + const workflow = makeWorkflow(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); + + // + // ACT + // + await authOwnerAgent + .post('/workflows') + .send({ ...workflow, projectId: project.id }) + // + // ASSERT + // + .expect(400, { + code: 400, + message: "You don't have the permissions to save the workflow in this project.", + }); + }); + + test('does not create workflow in other project if the user does not have the right role', async () => { + // + // ARRANGE + // + const projectRepository = Container.get(ProjectRepository); + const workflow = makeWorkflow(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); + await Container.get(ProjectService).addUser(project.id, owner.id, 'project:viewer'); + + // + // ACT + // + await authOwnerAgent + .post('/workflows') + .send({ ...workflow, projectId: project.id }) + // + // ASSERT + // + .expect(400, { + code: 400, + message: "You don't have the permissions to save the workflow in this project.", + }); + }); }); describe('GET /workflows/:id', () => { From 326532c4994bf4cdb12eab8997492d746406e2f3 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Fri, 16 Feb 2024 16:24:03 +0100 Subject: [PATCH 2/8] remove helper function to create projects using the repo is easy enough --- .../services/project.service.test.ts | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/cli/test/integration/services/project.service.test.ts b/packages/cli/test/integration/services/project.service.test.ts index b06012ac40897..2a37aa40e2617 100644 --- a/packages/cli/test/integration/services/project.service.test.ts +++ b/packages/cli/test/integration/services/project.service.test.ts @@ -3,10 +3,7 @@ import * as testDb from '../shared/testDb'; import Container from 'typedi'; import { createMember } from '../shared/db/users'; import { ProjectRepository } from '@/databases/repositories/project.repository'; -import type { DeepPartial } from 'ts-essentials'; -import type { Project } from '@/databases/entities/Project'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; -import { EntityNotFoundError } from '@n8n/typeorm'; import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { Scope } from '@n8n/permissions'; @@ -30,20 +27,6 @@ afterEach(async () => { await testDb.truncate(['User']); }); -async function createTeamProject(project?: DeepPartial) { - const projectRepository = Container.get(ProjectRepository); - - let projectEntity = projectRepository.create({ - name: 'Team Project', - ...project, - type: 'team', - }); - - projectEntity = await projectRepository.save(projectEntity); - - return projectEntity; -} - describe('ProjectService', () => { describe('addUser', () => { it.each([ @@ -58,7 +41,12 @@ describe('ProjectService', () => { // ARRANGE // const member = await createMember(); - const project = await createTeamProject(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); // // ACT @@ -79,7 +67,12 @@ describe('ProjectService', () => { // ARRANGE // const member = await createMember(); - const project = await createTeamProject(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); await projectService.addUser(project.id, member.id, 'project:viewer'); await projectRelationRepository.findOneOrFail({ @@ -117,7 +110,12 @@ describe('ProjectService', () => { // ARRANGE // const projectOwner = await createMember(); - const project = await createTeamProject(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); await projectService.addUser(project.id, projectOwner.id, role); // @@ -152,7 +150,12 @@ describe('ProjectService', () => { // ARRANGE // const projectViewer = await createMember(); - const project = await createTeamProject(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); await projectService.addUser(project.id, projectViewer.id, role); // @@ -176,7 +179,12 @@ describe('ProjectService', () => { // ARRANGE // const member = await createMember(); - const project = await createTeamProject(); + const project = await projectRepository.save( + projectRepository.create({ + name: 'Team Project', + type: 'team', + }), + ); // // ACT From 7ce9113efb69959f014ae9cb76422ffae8fa4fe1 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Fri, 16 Feb 2024 16:38:16 +0100 Subject: [PATCH 3/8] improve test names --- .../cli/test/integration/services/project.service.test.ts | 4 ++-- .../test/integration/workflows/workflows.controller.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/integration/services/project.service.test.ts b/packages/cli/test/integration/services/project.service.test.ts index 2a37aa40e2617..fe392755af3c6 100644 --- a/packages/cli/test/integration/services/project.service.test.ts +++ b/packages/cli/test/integration/services/project.service.test.ts @@ -62,7 +62,7 @@ describe('ProjectService', () => { }, ); - it('changes the role the user has to the project instead of creating a new relationship if the user already has a relationship to the project', async () => { + it('changes the role the user has in the project if the user is already part of the project, instead of creating a new relationship', async () => { // // ARRANGE // @@ -174,7 +174,7 @@ describe('ProjectService', () => { }, ); - it('should not return the project if the user is not related to it', async () => { + it('should not return the project if the user is not part of it', async () => { // // ARRANGE // diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index d7d8ff8640495..884f6da7c72fa 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -179,7 +179,7 @@ describe('POST /workflows', () => { }); }); - test('creates workflow in other project if projectId is passed', async () => { + test('creates workflow in a specific project if the projectId is passed', async () => { // // ARRANGE // @@ -212,7 +212,7 @@ describe('POST /workflows', () => { }); }); - test('does not create the workflow in specific project if the project is not shared with user', async () => { + test('does not create the workflow in a specific project if the user is not part of the project', async () => { // // ARRANGE // @@ -240,7 +240,7 @@ describe('POST /workflows', () => { }); }); - test('does not create workflow in other project if the user does not have the right role', async () => { + test('does not create the workflow in a specific project if the user does not have the right role to do so', async () => { // // ARRANGE // From 7985ed69ed77d2e66d0451845942d19b9ee2d434 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Tue, 20 Feb 2024 16:36:16 +0100 Subject: [PATCH 4/8] throw an applicaton error if the user has no personal project that way it get's reported to sentry --- packages/cli/src/workflows/workflows.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index d4dc5e432af29..027cc9996d736 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -49,6 +49,7 @@ import { WorkflowSharingService } from './workflowSharing.service'; import { UserManagementMailer } from '@/UserManagement/email'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectService } from '@/services/project.service'; +import { ApplicationError } from '../../../workflow/src'; @Authorized() @RestController('/workflows') @@ -138,7 +139,7 @@ export class WorkflowsController { // Safe guard in case the personal project does not exist for whatever reason. if (project === null) { - throw new InternalServerError('Failed to save workflow'); + throw new ApplicationError('No personal project found'); } const newSharedWorkflow = this.sharedWorkflowRepository.create({ From b1201d1a7917c629e490dae53a0497c2ff3dac08 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Tue, 20 Feb 2024 16:42:13 +0100 Subject: [PATCH 5/8] remove TODO --- packages/cli/src/workflows/workflows.controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 027cc9996d736..bf26170441997 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -127,8 +127,7 @@ export class WorkflowsController { const { projectId } = req.body; const project = projectId === undefined - ? // TODO: pass the transaction manager optionally? - await this.projectRepository.getPersonalProjectForUser(req.user.id) + ? await this.projectRepository.getPersonalProjectForUser(req.user.id) : await this.projectService.getProjectWithScope(req.user, projectId, 'workflow:create'); if (typeof projectId === 'string' && project === null) { From 7cc9d50e674fa46aca44f4d41b78838f1e13009b Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Tue, 20 Feb 2024 16:54:43 +0100 Subject: [PATCH 6/8] fix importing from outside the root dir --- packages/cli/src/workflows/workflows.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index bf26170441997..90e6feeaccc42 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -49,7 +49,7 @@ import { WorkflowSharingService } from './workflowSharing.service'; import { UserManagementMailer } from '@/UserManagement/email'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectService } from '@/services/project.service'; -import { ApplicationError } from '../../../workflow/src'; +import { ApplicationError } from 'n8n-workflow'; @Authorized() @RestController('/workflows') From c3e8353606971b036e46dbe895de6955753698ab Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Wed, 21 Feb 2024 11:11:40 +0100 Subject: [PATCH 7/8] remove duplicate dependency --- packages/cli/src/services/project.service.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/services/project.service.ts b/packages/cli/src/services/project.service.ts index 87b43c3ef90e2..5e78e681e708c 100644 --- a/packages/cli/src/services/project.service.ts +++ b/packages/cli/src/services/project.service.ts @@ -13,7 +13,6 @@ import { RoleService } from './role.service'; @Service() export class ProjectService { constructor( - private readonly projectsRepository: ProjectRepository, private readonly projectRepository: ProjectRepository, private readonly projectRelationRepository: ProjectRelationRepository, private readonly roleService: RoleService, @@ -22,9 +21,9 @@ export class ProjectService { async getAccessibleProjects(user: User): Promise { // This user is probably an admin, show them everything if (user.hasGlobalScope('project:read')) { - return await this.projectsRepository.find(); + return await this.projectRepository.find(); } - return await this.projectsRepository.getAccessibleProjects(user.id); + return await this.projectRepository.getAccessibleProjects(user.id); } async getPersonalProjectOwners(projectIds: string[]): Promise { @@ -45,7 +44,7 @@ export class ProjectService { } else if (pr) { name = pr.user.email; } - return this.projectsRepository.create({ + return this.projectRepository.create({ ...p, name, }); @@ -53,8 +52,8 @@ export class ProjectService { } async createTeamProject(name: string, adminUser: User): Promise { - const project = await this.projectsRepository.save( - this.projectsRepository.create({ + const project = await this.projectRepository.save( + this.projectRepository.create({ name, type: 'team', }), @@ -73,7 +72,7 @@ export class ProjectService { } async getPersonalProject(user: User): Promise { - return await this.projectsRepository.getPersonalProjectForUser(user.id); + return await this.projectRepository.getPersonalProjectForUser(user.id); } async getProjectRelationsForUser(user: User): Promise { @@ -87,7 +86,7 @@ export class ProjectService { projectId: string, relations: Array<{ userId: string; role: ProjectRole }>, ) { - const project = await this.projectsRepository.findOneOrFail({ + const project = await this.projectRepository.findOneOrFail({ where: { id: projectId, type: Not('personal') }, }); await this.projectRelationRepository.manager.transaction(async (em) => { From 0b07adb06d7e2e6c13b19df884a4650289e2cb4e Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Wed, 21 Feb 2024 16:14:24 +0100 Subject: [PATCH 8/8] find projects within the transaction --- .../src/databases/repositories/project.repository.ts | 7 +++++-- packages/cli/src/services/project.service.ts | 12 +++++++++--- packages/cli/src/workflows/workflows.controller.ts | 9 +++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/databases/repositories/project.repository.ts b/packages/cli/src/databases/repositories/project.repository.ts index a23688ae3c08f..6dfbdcccd7486 100644 --- a/packages/cli/src/databases/repositories/project.repository.ts +++ b/packages/cli/src/databases/repositories/project.repository.ts @@ -1,4 +1,5 @@ import { Service } from 'typedi'; +import type { EntityManager } from '@n8n/typeorm'; import { DataSource, Repository } from '@n8n/typeorm'; import { Project } from '../entities/Project'; @@ -8,8 +9,10 @@ export class ProjectRepository extends Repository { super(Project, dataSource.manager); } - async getPersonalProjectForUser(userId: string) { - return await this.findOne({ + async getPersonalProjectForUser(userId: string, entityManager?: EntityManager) { + const em = entityManager ?? this.manager; + + return await em.findOne(Project, { where: { type: 'personal', projectRelations: { userId, role: 'project:personalOwner' } }, }); } diff --git a/packages/cli/src/services/project.service.ts b/packages/cli/src/services/project.service.ts index 5e78e681e708c..09d16a95ec839 100644 --- a/packages/cli/src/services/project.service.ts +++ b/packages/cli/src/services/project.service.ts @@ -1,4 +1,4 @@ -import type { Project } from '@/databases/entities/Project'; +import { Project } from '@/databases/entities/Project'; import { ProjectRelation } from '@/databases/entities/ProjectRelation'; import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { User } from '@/databases/entities/User'; @@ -116,10 +116,16 @@ export class ProjectService { ); } - async getProjectWithScope(user: User, projectId: string, scope: Scope) { + async getProjectWithScope( + user: User, + projectId: string, + scope: Scope, + entityManager?: EntityManager, + ) { + const em = entityManager ?? this.projectRepository.manager; const projectRoles = this.roleService.rolesWithScope('project', [scope]); - return await this.projectRepository.findOne({ + return await em.findOne(Project, { where: { id: projectId, projectRelations: { diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 90e6feeaccc42..a78385bb01cfa 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -127,8 +127,13 @@ export class WorkflowsController { const { projectId } = req.body; const project = projectId === undefined - ? await this.projectRepository.getPersonalProjectForUser(req.user.id) - : await this.projectService.getProjectWithScope(req.user, projectId, 'workflow:create'); + ? await this.projectRepository.getPersonalProjectForUser(req.user.id, transactionManager) + : await this.projectService.getProjectWithScope( + req.user, + projectId, + 'workflow:create', + transactionManager, + ); if (typeof projectId === 'string' && project === null) { throw new BadRequestError(