From 30f35992cc74836800fe980de3b20c75174d0c8e Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 7 Jan 2025 16:42:12 +0100 Subject: [PATCH 1/6] feat(workspaces): filter discoverable workspaces with existing requests --- .../workspaces/repositories/workspaces.ts | 7 + .../tests/integration/repositories.spec.ts | 645 ++++++++++-------- 2 files changed, 357 insertions(+), 295 deletions(-) diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index 8741096b0..ec33dd472 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -38,6 +38,7 @@ import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace import { WorkspaceAcl as DbWorkspaceAcl, WorkspaceDomains, + WorkspaceJoinRequests, Workspaces } from '@/modules/workspaces/helpers/db' import { @@ -93,6 +94,12 @@ export const getUserDiscoverableWorkspacesFactory = 'acl.workspaceId', 'workspaces.id' ) + .leftJoin( + WorkspaceJoinRequests.name, + WorkspaceJoinRequests.col.workspaceId, + Workspaces.col.id + ) + .whereNull(WorkspaceJoinRequests.col.workspaceId) .whereIn('domain', domains) .where('discoverabilityEnabled', true) .where('verified', true) diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts index 4e6b2201b..24e0099dd 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -43,6 +43,7 @@ import { } from '@/modules/core/repositories/streams' import { omit } from 'lodash' import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces' +import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' const getWorkspace = getWorkspaceFactory({ db }) const getWorkspaceBySlug = getWorkspaceBySlugFactory({ db }) @@ -771,317 +772,371 @@ describe('Workspace repositories', () => { expect(workspaces.length).to.equal(1) }) - }) - describe('getWorkspaceDomainsFactory creates a function, that', () => { - it('returns a workspace with domains', async () => { - const user = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomPassword() - } - await createTestUser(user) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: user.id - } - await createTestWorkspace(workspace, user) + it('should not return discoverable workspaces with existing requests for the user', async () => { + const user = await createAndStoreTestUser() + await updateUserEmail({ + query: { + email: user.email + }, + update: { + verified: true + } + }) - await storeWorkspaceDomainFactory({ db })({ + const workspace = await createAndStoreTestWorkspace({ + discoverabilityEnabled: true + }) + await storeWorkspaceDomain({ workspaceDomain: { - id: createRandomPassword(), + id: cryptoRandomString({ length: 6 }), domain: 'example.org', - verified: true, workspaceId: workspace.id, + verified: true, createdAt: new Date(), updatedAt: new Date(), createdByUserId: user.id } }) - const workspaceWithDomains = await getWorkspaceWithDomainsFactory({ db })({ - id: workspace.id + const workspaceWithExistingRequest = await createAndStoreTestWorkspace({ + discoverabilityEnabled: true }) - expect(workspaceWithDomains?.domains.length).to.eq(1) - }) - }) - - describe('countWorkspaceRoleWithOptionalProjectRoleFactory returns a function, that', () => { - it('counts workspace roles by userId', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace, admin) - - // just another workspace, for testing if workspaceId filter works - const workspace2 = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace2, admin) - - const admin2 = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin2) - await assignToWorkspace(workspace, admin2, Roles.Workspace.Admin) - - const member = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member) - await assignToWorkspace(workspace, member, Roles.Workspace.Member) - let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Admin + await storeWorkspaceDomain({ + workspaceDomain: { + id: cryptoRandomString({ length: 6 }), + domain: 'example.org', + workspaceId: workspaceWithExistingRequest.id, + verified: true, + createdAt: new Date(), + updatedAt: new Date(), + createdByUserId: user.id + } }) - expect(count).to.equal(2) - - count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Member - }) - expect(count).to.equal(1) - }) - it('counts workspace roles with a project role filter', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - - await createTestWorkspace(workspace, admin) - - const member = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member) - await assignToWorkspace(workspace, member, Roles.Workspace.Member) - - const member2 = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member2) - await assignToWorkspace(workspace, member2, Roles.Workspace.Member) - - const project1 = { - id: createRandomString(), - name: 'test stream', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace.id - } - const project2 = { - id: createRandomString(), - name: 'test stream 2', - isPublic: true, - ownerId: member.id, - workspaceId: workspace.id - } - - const project3 = { - id: createRandomString(), - name: 'test stream 3', - isPublic: true, - ownerId: member.id, - workspaceId: workspace.id - } - await createTestStream(project1, admin) - await createTestStream(project2, member) - await createTestStream(project3, member2) - - let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Admin, - projectRole: Roles.Stream.Owner - }) - expect(count).to.equal(1) - - count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Member, - projectRole: Roles.Stream.Owner - }) - expect(count).to.equal(2) - }) - it('does not count project roles, that are not in the workspace', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace, admin) - - const guest = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(guest) - await assignToWorkspace(workspace, guest, Roles.Workspace.Guest) - - const guest2 = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(guest2) - await assignToWorkspace(workspace, guest2, Roles.Workspace.Guest) - - // only project 1 is in the workspace - const project1 = { - id: createRandomString(), - name: 'test stream', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace.id - } - // this is not in the workspace, roles here should not count - const project2 = { - id: createRandomString(), - name: 'test stream 2', - isPublic: true, - ownerId: guest.id - } - - await createTestStream(project1, admin) - await createTestStream(project2, guest) - - // adding project roles to guests - await upsertProjectRole({ - role: Roles.Stream.Contributor, - projectId: project1.id, - userId: guest.id + await db(WorkspaceJoinRequests.name).insert({ + workspaceId: workspaceWithExistingRequest.id, + userId: user.id, + createdAt: new Date(), + status: 'pending' }) - await upsertProjectRole({ - role: Roles.Stream.Reviewer, - projectId: project1.id, - userId: guest2.id + const workspaces = await getUserDiscoverableWorkspaces({ + domains: ['example.org'], + userId: user.id }) - // adding contributor to guest 2 on project 2 - await upsertProjectRole({ - role: Roles.Stream.Contributor, - projectId: project2.id, - userId: guest2.id - }) - - const count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Guest, - projectRole: Roles.Stream.Contributor - }) - // checking that the non workspace project doesn't leak into the counts - expect(count).to.equal(1) - }) - it('does not count roles from other workspaces when filtering by project role too', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace1 = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace1, admin) - - const workspace2 = { - id: createRandomPassword(), - name: 'my workspace 2', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace2, admin) - - const member = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member) - await assignToWorkspace(workspace1, member, Roles.Workspace.Member) - // member becomes a guest in the other workspace and it leaks back into the first - await assignToWorkspace(workspace2, member, Roles.Workspace.Guest) - - const project1 = { - id: createRandomString(), - name: 'test stream', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace1.id - } - // this is not in the workspace, roles here should not count - const project2 = { - id: createRandomString(), - name: 'test stream 2', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace2.id - } - - await createTestStream(project1, admin) - await createTestStream(project2, admin) - - await grantStreamPermissions({ - role: Roles.Stream.Contributor, - streamId: project2.id, - userId: member.id - }) - - let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace1.id, - workspaceRole: Roles.Workspace.Guest, - projectRole: Roles.Stream.Contributor - }) - - expect(count).to.equal(0) - - count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace2.id, - workspaceRole: Roles.Workspace.Guest, - projectRole: Roles.Stream.Contributor - }) - - expect(count).to.equal(1) + expect(workspaces.length).to.equal(1) }) }) }) + +describe('getWorkspaceDomainsFactory creates a function, that', () => { + it('returns a workspace with domains', async () => { + const user = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomPassword() + } + await createTestUser(user) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: user.id + } + await createTestWorkspace(workspace, user) + + await storeWorkspaceDomainFactory({ db })({ + workspaceDomain: { + id: createRandomPassword(), + domain: 'example.org', + verified: true, + workspaceId: workspace.id, + createdAt: new Date(), + updatedAt: new Date(), + createdByUserId: user.id + } + }) + const workspaceWithDomains = await getWorkspaceWithDomainsFactory({ db })({ + id: workspace.id + }) + expect(workspaceWithDomains?.domains.length).to.eq(1) + }) +}) + +describe('countWorkspaceRoleWithOptionalProjectRoleFactory returns a function, that', () => { + it('counts workspace roles by userId', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + // just another workspace, for testing if workspaceId filter works + const workspace2 = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace2, admin) + + const admin2 = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin2) + await assignToWorkspace(workspace, admin2, Roles.Workspace.Admin) + + const member = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Admin + }) + expect(count).to.equal(2) + + count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Member + }) + expect(count).to.equal(1) + }) + it('counts workspace roles with a project role filter', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + + await createTestWorkspace(workspace, admin) + + const member = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + + const member2 = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member2) + await assignToWorkspace(workspace, member2, Roles.Workspace.Member) + + const project1 = { + id: createRandomString(), + name: 'test stream', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace.id + } + const project2 = { + id: createRandomString(), + name: 'test stream 2', + isPublic: true, + ownerId: member.id, + workspaceId: workspace.id + } + + const project3 = { + id: createRandomString(), + name: 'test stream 3', + isPublic: true, + ownerId: member.id, + workspaceId: workspace.id + } + await createTestStream(project1, admin) + await createTestStream(project2, member) + await createTestStream(project3, member2) + + let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Admin, + projectRole: Roles.Stream.Owner + }) + expect(count).to.equal(1) + + count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Member, + projectRole: Roles.Stream.Owner + }) + expect(count).to.equal(2) + }) + it('does not count project roles, that are not in the workspace', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + const guest = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(guest) + await assignToWorkspace(workspace, guest, Roles.Workspace.Guest) + + const guest2 = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(guest2) + await assignToWorkspace(workspace, guest2, Roles.Workspace.Guest) + + // only project 1 is in the workspace + const project1 = { + id: createRandomString(), + name: 'test stream', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace.id + } + // this is not in the workspace, roles here should not count + const project2 = { + id: createRandomString(), + name: 'test stream 2', + isPublic: true, + ownerId: guest.id + } + + await createTestStream(project1, admin) + await createTestStream(project2, guest) + + // adding project roles to guests + await upsertProjectRole({ + role: Roles.Stream.Contributor, + projectId: project1.id, + userId: guest.id + }) + + await upsertProjectRole({ + role: Roles.Stream.Reviewer, + projectId: project1.id, + userId: guest2.id + }) + + // adding contributor to guest 2 on project 2 + await upsertProjectRole({ + role: Roles.Stream.Contributor, + projectId: project2.id, + userId: guest2.id + }) + + const count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Guest, + projectRole: Roles.Stream.Contributor + }) + // checking that the non workspace project doesn't leak into the counts + expect(count).to.equal(1) + }) + it('does not count roles from other workspaces when filtering by project role too', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace1 = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace1, admin) + + const workspace2 = { + id: createRandomPassword(), + name: 'my workspace 2', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace2, admin) + + const member = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member) + await assignToWorkspace(workspace1, member, Roles.Workspace.Member) + // member becomes a guest in the other workspace and it leaks back into the first + await assignToWorkspace(workspace2, member, Roles.Workspace.Guest) + + const project1 = { + id: createRandomString(), + name: 'test stream', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace1.id + } + // this is not in the workspace, roles here should not count + const project2 = { + id: createRandomString(), + name: 'test stream 2', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace2.id + } + + await createTestStream(project1, admin) + await createTestStream(project2, admin) + + await grantStreamPermissions({ + role: Roles.Stream.Contributor, + streamId: project2.id, + userId: member.id + }) + + let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace1.id, + workspaceRole: Roles.Workspace.Guest, + projectRole: Roles.Stream.Contributor + }) + + expect(count).to.equal(0) + + count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace2.id, + workspaceRole: Roles.Workspace.Guest, + projectRole: Roles.Stream.Contributor + }) + + expect(count).to.equal(1) + }) +}) From 844133f435c470dab8351bc2b82445b503fd3c1f Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 7 Jan 2025 17:34:12 +0100 Subject: [PATCH 2/6] feat(workspaces): fix query --- .../workspaces/repositories/workspaces.ts | 18 ++++++++++++------ .../tests/integration/repositories.spec.ts | 18 +++++++++++++++++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index ec33dd472..6adb2c56c 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -2,6 +2,7 @@ import { Workspace, WorkspaceAcl, WorkspaceDomain, + WorkspaceJoinRequest, WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types' import { @@ -38,7 +39,6 @@ import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace import { WorkspaceAcl as DbWorkspaceAcl, WorkspaceDomains, - WorkspaceJoinRequests, Workspaces } from '@/modules/workspaces/helpers/db' import { @@ -68,7 +68,9 @@ const tables = { workspaceDomains: (db: Knex) => db('workspace_domains'), workspacesAcl: (db: Knex) => db('workspace_acl'), workspaceCreationState: (db: Knex) => - db('workspace_creation_state') + db('workspace_creation_state'), + workspaceJoinRequests: (db: Knex) => + db('workspace_join_requests') } export const getUserDiscoverableWorkspacesFactory = @@ -95,11 +97,15 @@ export const getUserDiscoverableWorkspacesFactory = 'workspaces.id' ) .leftJoin( - WorkspaceJoinRequests.name, - WorkspaceJoinRequests.col.workspaceId, - Workspaces.col.id + tables + .workspaceJoinRequests(db) + .select('*') + .where({ userId }) + .as('joinRequest'), + 'joinRequest.workspaceId', + 'workspaces.id' ) - .whereNull(WorkspaceJoinRequests.col.workspaceId) + .whereNull('joinRequest.workspaceId') .whereIn('domain', domains) .where('discoverabilityEnabled', true) .where('verified', true) diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts index 24e0099dd..91d1dd38c 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -773,7 +773,7 @@ describe('Workspace repositories', () => { expect(workspaces.length).to.equal(1) }) - it('should not return discoverable workspaces with existing requests for the user', async () => { + it.only('should not return discoverable workspaces with existing requests for the user', async () => { const user = await createAndStoreTestUser() await updateUserEmail({ query: { @@ -783,6 +783,15 @@ describe('Workspace repositories', () => { verified: true } }) + const otherUser = await createAndStoreTestUser() + await updateUserEmail({ + query: { + email: otherUser.email + }, + update: { + verified: true + } + }) const workspace = await createAndStoreTestWorkspace({ discoverabilityEnabled: true @@ -798,6 +807,13 @@ describe('Workspace repositories', () => { createdByUserId: user.id } }) + // existing request for other user + await db(WorkspaceJoinRequests.name).insert({ + workspaceId: workspace.id, + userId: otherUser.id, + createdAt: new Date(), + status: 'pending' + }) const workspaceWithExistingRequest = await createAndStoreTestWorkspace({ discoverabilityEnabled: true }) From c25418a1d207c158bd952efec477f6e284682f53 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Thu, 9 Jan 2025 09:49:39 +0100 Subject: [PATCH 3/6] feat(workspaces): test indentation fix --- .../tests/integration/repositories.spec.ts | 614 +++++++++--------- 1 file changed, 307 insertions(+), 307 deletions(-) diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts index 91d1dd38c..1588ef002 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -773,7 +773,7 @@ describe('Workspace repositories', () => { expect(workspaces.length).to.equal(1) }) - it.only('should not return discoverable workspaces with existing requests for the user', async () => { + it('should not return discoverable workspaces with existing requests for the user', async () => { const user = await createAndStoreTestUser() await updateUserEmail({ query: { @@ -843,316 +843,316 @@ describe('Workspace repositories', () => { expect(workspaces.length).to.equal(1) }) }) -}) -describe('getWorkspaceDomainsFactory creates a function, that', () => { - it('returns a workspace with domains', async () => { - const user = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomPassword() - } - await createTestUser(user) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: user.id - } - await createTestWorkspace(workspace, user) - - await storeWorkspaceDomainFactory({ db })({ - workspaceDomain: { + describe('getWorkspaceDomainsFactory creates a function, that', () => { + it('returns a workspace with domains', async () => { + const user = { id: createRandomPassword(), - domain: 'example.org', - verified: true, - workspaceId: workspace.id, - createdAt: new Date(), - updatedAt: new Date(), - createdByUserId: user.id + name: createRandomPassword(), + email: createRandomPassword() } + await createTestUser(user) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: user.id + } + await createTestWorkspace(workspace, user) + + await storeWorkspaceDomainFactory({ db })({ + workspaceDomain: { + id: createRandomPassword(), + domain: 'example.org', + verified: true, + workspaceId: workspace.id, + createdAt: new Date(), + updatedAt: new Date(), + createdByUserId: user.id + } + }) + const workspaceWithDomains = await getWorkspaceWithDomainsFactory({ db })({ + id: workspace.id + }) + expect(workspaceWithDomains?.domains.length).to.eq(1) }) - const workspaceWithDomains = await getWorkspaceWithDomainsFactory({ db })({ - id: workspace.id + }) + + describe('countWorkspaceRoleWithOptionalProjectRoleFactory returns a function, that', () => { + it('counts workspace roles by userId', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + // just another workspace, for testing if workspaceId filter works + const workspace2 = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace2, admin) + + const admin2 = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin2) + await assignToWorkspace(workspace, admin2, Roles.Workspace.Admin) + + const member = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Admin + }) + expect(count).to.equal(2) + + count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Member + }) + expect(count).to.equal(1) + }) + it('counts workspace roles with a project role filter', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + + await createTestWorkspace(workspace, admin) + + const member = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + + const member2 = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member2) + await assignToWorkspace(workspace, member2, Roles.Workspace.Member) + + const project1 = { + id: createRandomString(), + name: 'test stream', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace.id + } + const project2 = { + id: createRandomString(), + name: 'test stream 2', + isPublic: true, + ownerId: member.id, + workspaceId: workspace.id + } + + const project3 = { + id: createRandomString(), + name: 'test stream 3', + isPublic: true, + ownerId: member.id, + workspaceId: workspace.id + } + await createTestStream(project1, admin) + await createTestStream(project2, member) + await createTestStream(project3, member2) + + let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Admin, + projectRole: Roles.Stream.Owner + }) + expect(count).to.equal(1) + + count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Member, + projectRole: Roles.Stream.Owner + }) + expect(count).to.equal(2) + }) + it('does not count project roles, that are not in the workspace', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + const guest = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(guest) + await assignToWorkspace(workspace, guest, Roles.Workspace.Guest) + + const guest2 = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(guest2) + await assignToWorkspace(workspace, guest2, Roles.Workspace.Guest) + + // only project 1 is in the workspace + const project1 = { + id: createRandomString(), + name: 'test stream', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace.id + } + // this is not in the workspace, roles here should not count + const project2 = { + id: createRandomString(), + name: 'test stream 2', + isPublic: true, + ownerId: guest.id + } + + await createTestStream(project1, admin) + await createTestStream(project2, guest) + + // adding project roles to guests + await upsertProjectRole({ + role: Roles.Stream.Contributor, + projectId: project1.id, + userId: guest.id + }) + + await upsertProjectRole({ + role: Roles.Stream.Reviewer, + projectId: project1.id, + userId: guest2.id + }) + + // adding contributor to guest 2 on project 2 + await upsertProjectRole({ + role: Roles.Stream.Contributor, + projectId: project2.id, + userId: guest2.id + }) + + const count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace.id, + workspaceRole: Roles.Workspace.Guest, + projectRole: Roles.Stream.Contributor + }) + // checking that the non workspace project doesn't leak into the counts + expect(count).to.equal(1) + }) + it('does not count roles from other workspaces when filtering by project role too', async () => { + const admin = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(admin) + const workspace1 = { + id: createRandomPassword(), + name: 'my workspace', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace1, admin) + + const workspace2 = { + id: createRandomPassword(), + name: 'my workspace 2', + slug: cryptoRandomString({ length: 10 }), + ownerId: admin.id + } + await createTestWorkspace(workspace2, admin) + + const member = { + id: createRandomPassword(), + name: createRandomPassword(), + email: createRandomEmail() + } + await createTestUser(member) + await assignToWorkspace(workspace1, member, Roles.Workspace.Member) + // member becomes a guest in the other workspace and it leaks back into the first + await assignToWorkspace(workspace2, member, Roles.Workspace.Guest) + + const project1 = { + id: createRandomString(), + name: 'test stream', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace1.id + } + // this is not in the workspace, roles here should not count + const project2 = { + id: createRandomString(), + name: 'test stream 2', + isPublic: true, + ownerId: admin.id, + workspaceId: workspace2.id + } + + await createTestStream(project1, admin) + await createTestStream(project2, admin) + + await grantStreamPermissions({ + role: Roles.Stream.Contributor, + streamId: project2.id, + userId: member.id + }) + + let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace1.id, + workspaceRole: Roles.Workspace.Guest, + projectRole: Roles.Stream.Contributor + }) + + expect(count).to.equal(0) + + count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ + workspaceId: workspace2.id, + workspaceRole: Roles.Workspace.Guest, + projectRole: Roles.Stream.Contributor + }) + + expect(count).to.equal(1) }) - expect(workspaceWithDomains?.domains.length).to.eq(1) - }) -}) - -describe('countWorkspaceRoleWithOptionalProjectRoleFactory returns a function, that', () => { - it('counts workspace roles by userId', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace, admin) - - // just another workspace, for testing if workspaceId filter works - const workspace2 = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace2, admin) - - const admin2 = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin2) - await assignToWorkspace(workspace, admin2, Roles.Workspace.Admin) - - const member = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member) - await assignToWorkspace(workspace, member, Roles.Workspace.Member) - let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Admin - }) - expect(count).to.equal(2) - - count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Member - }) - expect(count).to.equal(1) - }) - it('counts workspace roles with a project role filter', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - - await createTestWorkspace(workspace, admin) - - const member = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member) - await assignToWorkspace(workspace, member, Roles.Workspace.Member) - - const member2 = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member2) - await assignToWorkspace(workspace, member2, Roles.Workspace.Member) - - const project1 = { - id: createRandomString(), - name: 'test stream', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace.id - } - const project2 = { - id: createRandomString(), - name: 'test stream 2', - isPublic: true, - ownerId: member.id, - workspaceId: workspace.id - } - - const project3 = { - id: createRandomString(), - name: 'test stream 3', - isPublic: true, - ownerId: member.id, - workspaceId: workspace.id - } - await createTestStream(project1, admin) - await createTestStream(project2, member) - await createTestStream(project3, member2) - - let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Admin, - projectRole: Roles.Stream.Owner - }) - expect(count).to.equal(1) - - count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Member, - projectRole: Roles.Stream.Owner - }) - expect(count).to.equal(2) - }) - it('does not count project roles, that are not in the workspace', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace, admin) - - const guest = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(guest) - await assignToWorkspace(workspace, guest, Roles.Workspace.Guest) - - const guest2 = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(guest2) - await assignToWorkspace(workspace, guest2, Roles.Workspace.Guest) - - // only project 1 is in the workspace - const project1 = { - id: createRandomString(), - name: 'test stream', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace.id - } - // this is not in the workspace, roles here should not count - const project2 = { - id: createRandomString(), - name: 'test stream 2', - isPublic: true, - ownerId: guest.id - } - - await createTestStream(project1, admin) - await createTestStream(project2, guest) - - // adding project roles to guests - await upsertProjectRole({ - role: Roles.Stream.Contributor, - projectId: project1.id, - userId: guest.id - }) - - await upsertProjectRole({ - role: Roles.Stream.Reviewer, - projectId: project1.id, - userId: guest2.id - }) - - // adding contributor to guest 2 on project 2 - await upsertProjectRole({ - role: Roles.Stream.Contributor, - projectId: project2.id, - userId: guest2.id - }) - - const count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace.id, - workspaceRole: Roles.Workspace.Guest, - projectRole: Roles.Stream.Contributor - }) - // checking that the non workspace project doesn't leak into the counts - expect(count).to.equal(1) - }) - it('does not count roles from other workspaces when filtering by project role too', async () => { - const admin = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(admin) - const workspace1 = { - id: createRandomPassword(), - name: 'my workspace', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace1, admin) - - const workspace2 = { - id: createRandomPassword(), - name: 'my workspace 2', - slug: cryptoRandomString({ length: 10 }), - ownerId: admin.id - } - await createTestWorkspace(workspace2, admin) - - const member = { - id: createRandomPassword(), - name: createRandomPassword(), - email: createRandomEmail() - } - await createTestUser(member) - await assignToWorkspace(workspace1, member, Roles.Workspace.Member) - // member becomes a guest in the other workspace and it leaks back into the first - await assignToWorkspace(workspace2, member, Roles.Workspace.Guest) - - const project1 = { - id: createRandomString(), - name: 'test stream', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace1.id - } - // this is not in the workspace, roles here should not count - const project2 = { - id: createRandomString(), - name: 'test stream 2', - isPublic: true, - ownerId: admin.id, - workspaceId: workspace2.id - } - - await createTestStream(project1, admin) - await createTestStream(project2, admin) - - await grantStreamPermissions({ - role: Roles.Stream.Contributor, - streamId: project2.id, - userId: member.id - }) - - let count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace1.id, - workspaceRole: Roles.Workspace.Guest, - projectRole: Roles.Stream.Contributor - }) - - expect(count).to.equal(0) - - count = await countWorkspaceRoleWithOptionalProjectRoleFactory({ db })({ - workspaceId: workspace2.id, - workspaceRole: Roles.Workspace.Guest, - projectRole: Roles.Stream.Contributor - }) - - expect(count).to.equal(1) }) }) From c32026b6d6ce26e71218d1bc0febc1b15c220ff6 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Thu, 9 Jan 2025 11:21:46 +0100 Subject: [PATCH 4/6] feat(workspaces): dismiss workspace mutation --- .../typedefs/workspaces.graphql | 5 ++ .../modules/core/graph/generated/graphql.ts | 13 ++++ .../graph/generated/graphql.ts | 10 +++ .../modules/workspaces/domain/operations.ts | 8 +++ .../modules/workspaces/errors/workspace.ts | 6 ++ .../workspaces/graph/resolvers/workspaces.ts | 11 ++- .../repositories/workspaceJoinRequests.ts | 19 ++++++ .../services/workspaceJoinRequests.ts | 20 ++++++ .../workspaces/tests/helpers/graphql.ts | 8 +++ .../workspaceJoinReqquests.spec.ts | 67 +++++++++++++++++++ .../integration/workspaces.graph.spec.ts | 44 +++++++++++- .../server/test/graphql/generated/graphql.ts | 18 +++++ 12 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts create mode 100644 packages/server/modules/workspaces/services/workspaceJoinRequests.ts create mode 100644 packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts diff --git a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql index 37a84b4af..1687d60e7 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql @@ -151,6 +151,11 @@ type WorkspaceMutations { invites: WorkspaceInviteMutations! projects: WorkspaceProjectMutations! @hasServerRole(role: SERVER_USER) updateCreationState(input: WorkspaceCreationStateInput!): Boolean! + dismiss(input: WorkspaceDismissInput!): Boolean! @hasServerRole(role: SERVER_USER) +} + +input WorkspaceDismissInput { + workspaceId: ID! } input WorkspaceCreationStateInput { diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 224150016..e48622f6b 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -4297,6 +4297,10 @@ export type WorkspaceCreationStateInput = { workspaceId: Scalars['ID']['input']; }; +export type WorkspaceDismissInput = { + workspaceId: Scalars['ID']['input']; +}; + export type WorkspaceDomain = { __typename?: 'WorkspaceDomain'; domain: Scalars['String']['output']; @@ -4390,6 +4394,7 @@ export type WorkspaceMutations = { delete: Scalars['Boolean']['output']; deleteDomain: Workspace; deleteSsoProvider: Scalars['Boolean']['output']; + dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; join: Workspace; leave: Scalars['Boolean']['output']; @@ -4427,6 +4432,11 @@ export type WorkspaceMutationsDeleteSsoProviderArgs = { }; +export type WorkspaceMutationsDismissArgs = { + input: WorkspaceDismissInput; +}; + + export type WorkspaceMutationsJoinArgs = { input: JoinWorkspaceInput; }; @@ -4962,6 +4972,7 @@ export type ResolversTypes = { WorkspaceCreateInput: WorkspaceCreateInput; WorkspaceCreationState: ResolverTypeWrapper; WorkspaceCreationStateInput: WorkspaceCreationStateInput; + WorkspaceDismissInput: WorkspaceDismissInput; WorkspaceDomain: ResolverTypeWrapper; WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput; WorkspaceFeatureName: WorkspaceFeatureName; @@ -5234,6 +5245,7 @@ export type ResolversParentTypes = { WorkspaceCreateInput: WorkspaceCreateInput; WorkspaceCreationState: WorkspaceCreationState; WorkspaceCreationStateInput: WorkspaceCreationStateInput; + WorkspaceDismissInput: WorkspaceDismissInput; WorkspaceDomain: WorkspaceDomain; WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput; WorkspaceInviteCreateInput: WorkspaceInviteCreateInput; @@ -6768,6 +6780,7 @@ export type WorkspaceMutationsResolvers>; deleteDomain?: Resolver>; deleteSsoProvider?: Resolver>; + dismiss?: Resolver>; invites?: Resolver; join?: Resolver>; leave?: Resolver>; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 807a1944f..a139d0574 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4278,6 +4278,10 @@ export type WorkspaceCreationStateInput = { workspaceId: Scalars['ID']['input']; }; +export type WorkspaceDismissInput = { + workspaceId: Scalars['ID']['input']; +}; + export type WorkspaceDomain = { __typename?: 'WorkspaceDomain'; domain: Scalars['String']['output']; @@ -4371,6 +4375,7 @@ export type WorkspaceMutations = { delete: Scalars['Boolean']['output']; deleteDomain: Workspace; deleteSsoProvider: Scalars['Boolean']['output']; + dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; join: Workspace; leave: Scalars['Boolean']['output']; @@ -4408,6 +4413,11 @@ export type WorkspaceMutationsDeleteSsoProviderArgs = { }; +export type WorkspaceMutationsDismissArgs = { + input: WorkspaceDismissInput; +}; + + export type WorkspaceMutationsJoinArgs = { input: JoinWorkspaceInput; }; diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index 66e39b30f..8ce7e2c21 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -4,6 +4,8 @@ import { Workspace, WorkspaceAcl, WorkspaceDomain, + WorkspaceJoinRequest, + WorkspaceJoinRequestStatus, WorkspaceRegionAssignment, WorkspaceWithDomains, WorkspaceWithOptionalRole @@ -308,3 +310,9 @@ export type GetWorkspaceCreationState = (params: { export type UpsertWorkspaceCreationState = (params: { workspaceCreationState: WorkspaceCreationState }) => Promise + +export type UpdateWorkspaceJoinRequestStatus = (params: { + workspaceId: string + userId: string + status: WorkspaceJoinRequestStatus +}) => Promise | undefined> diff --git a/packages/server/modules/workspaces/errors/workspace.ts b/packages/server/modules/workspaces/errors/workspace.ts index 5a81b76b1..19d2b012b 100644 --- a/packages/server/modules/workspaces/errors/workspace.ts +++ b/packages/server/modules/workspaces/errors/workspace.ts @@ -119,3 +119,9 @@ export class WorkspacePaidPlanActiveError extends BaseError { static code = 'WORKSPACE_PAID_PLAN_ACTIVE' static statusCode = 400 } + +export class WorkspaceJoinRequestNotFoundError extends BaseError { + static defaultMessage = 'Joinable workspace not found' + static code = 'WORKSPACE_JOIN_REQUEST_NOT_FOUND_ERROR' + static statusCode = 404 +} diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index a0ab38e23..da269a6f4 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -203,6 +203,8 @@ import { Knex } from 'knex' import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems' import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing' import { BadRequestError } from '@/modules/shared/errors' +import { dismissWorkspaceJoinRequestFactory } from '@/modules/workspaces/services/workspaceJoinRequests' +import { updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' const eventBus = getEventBus() const getServerInfo = getServerInfoFactory({ db }) @@ -774,7 +776,14 @@ export = FF_WORKSPACES_MODULE_ENABLED return true }, invites: () => ({}), - projects: () => ({}) + projects: () => ({}), + dismiss: async (_parent, args, ctx) => { + return await dismissWorkspaceJoinRequestFactory({ + updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({ + db + }) + })({ userId: ctx.userId!, workspaceId: args.input.workspaceId }) + } }, WorkspaceInviteMutations: { resend: async (_parent, args, ctx) => { diff --git a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts new file mode 100644 index 000000000..a8a178231 --- /dev/null +++ b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts @@ -0,0 +1,19 @@ +import { UpdateWorkspaceJoinRequestStatus } from '@/modules/workspaces/domain/operations' +import { WorkspaceJoinRequest } from '@/modules/workspacesCore/domain/types' +import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' +import { Knex } from 'knex' + +const tables = { + workspaceJoinRequests: (db: Knex) => + db(WorkspaceJoinRequests.name) +} + +export const updateWorkspaceJoinRequestStatusFactory = + ({ db }: { db: Knex }): UpdateWorkspaceJoinRequestStatus => + async ({ workspaceId, userId, status }) => { + const [request] = await tables + .workspaceJoinRequests(db) + .update({ status }, ['workspaceId', 'userId']) + .where({ workspaceId, userId }) + return request + } diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts new file mode 100644 index 000000000..a94fa5bf7 --- /dev/null +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -0,0 +1,20 @@ +import { UpdateWorkspaceJoinRequestStatus } from '@/modules/workspaces/domain/operations' +import { WorkspaceJoinRequestNotFoundError } from '@/modules/workspaces/errors/workspace' + +export const dismissWorkspaceJoinRequestFactory = + ({ + updateWorkspaceJoinRequestStatus + }: { + updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus + }) => + async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { + const updated = await updateWorkspaceJoinRequestStatus({ + userId, + workspaceId, + status: 'dismissed' + }) + if (!updated) { + throw new WorkspaceJoinRequestNotFoundError() + } + return true + } diff --git a/packages/server/modules/workspaces/tests/helpers/graphql.ts b/packages/server/modules/workspaces/tests/helpers/graphql.ts index adb9f9558..f6a626677 100644 --- a/packages/server/modules/workspaces/tests/helpers/graphql.ts +++ b/packages/server/modules/workspaces/tests/helpers/graphql.ts @@ -315,3 +315,11 @@ export const onWorkspaceUpdatedSubscription = gql` ${basicWorkspaceFragment} ` + +export const dismissWorkspaceMutation = gql` + mutation dismissWorkspace($input: WorkspaceDismissInput!) { + workspaceMutations { + dismiss(input: $input) + } + } +` diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts new file mode 100644 index 000000000..ac2335486 --- /dev/null +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts @@ -0,0 +1,67 @@ +import { db } from '@/db/knex' +import { createRandomString } from '@/modules/core/helpers/testHelpers' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' +import { updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' +import { dismissWorkspaceJoinRequestFactory } from '@/modules/workspaces/services/workspaceJoinRequests' +import { + BasicTestWorkspace, + createTestWorkspace +} from '@/modules/workspaces/tests/helpers/creation' +import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' +import { expectToThrow } from '@/test/assertionHelper' +import { BasicTestUser, createTestUser } from '@/test/authHelper' +import { Roles } from '@speckle/shared' +import { expect } from 'chai' +import cryptoRandomString from 'crypto-random-string' + +const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() + +;(FF_WORKSPACES_MODULE_ENABLED ? describe : describe.skip)( + 'workspaceJoinRequests services', + () => { + describe('dismissWorkspaceJoinRequestFactory, returns a function that ', () => { + it('throws a WorkspaceJoinRequestNotFoundError if the updateWorkspaceJoinRequestStatus does not exists', async () => { + expectToThrow(() => + dismissWorkspaceJoinRequestFactory({ + updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({ + db + }) + })({ workspaceId: createRandomString(), userId: createRandomString() }) + ) + }) + it('marks the request as dismissed', async () => { + const user: BasicTestUser = { + id: '', + name: 'John Speckle', + email: 'john-speckle@example.org', + role: Roles.Server.Admin, + verified: true + } + + await createTestUser(user) + + const workspace: BasicTestWorkspace = { + id: '', + slug: '', + ownerId: '', + name: cryptoRandomString({ length: 6 }), + description: cryptoRandomString({ length: 12 }) + } + await createTestWorkspace(workspace, user) + + await db(WorkspaceJoinRequests.name).insert({ + workspaceId: workspace.id, + userId: user.id, + status: 'pending' + }) + expect( + await dismissWorkspaceJoinRequestFactory({ + updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({ + db + }) + })({ workspaceId: workspace.id, userId: user.id }) + ).to.equal(true) + }) + }) + } +) diff --git a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts index d13008f38..38cd06d30 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts @@ -26,7 +26,8 @@ import { GetWorkspaceWithProjectsDocument, AddWorkspaceDomainDocument, DeleteWorkspaceDomainDocument, - CreateWorkspaceProjectDocument + CreateWorkspaceProjectDocument, + DismissWorkspaceDocument } from '@/test/graphql/generated/graphql' import { beforeEachContext } from '@/test/hooks' import { AllScopes } from '@/modules/core/helpers/mainConstants' @@ -47,6 +48,8 @@ import { } from '@/modules/core/helpers/testHelpers' import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' +import { WorkspaceJoinRequestNotFoundError } from '@/modules/workspaces/errors/workspace' +import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' const grantStreamPermissions = grantStreamPermissionsFactory({ db }) @@ -834,6 +837,45 @@ describe('Workspaces GQL CRUD', () => { expect(resC).to.haveGraphQLErrors('Provided default project role is invalid') }) }) + + describe('mutation workspaceMutations.dismiss', () => { + it('should return an error if workspace does not exists', async () => { + const res = await apollo.execute(DismissWorkspaceDocument, { + input: { + workspaceId: cryptoRandomString({ length: 6 }) + } + }) + expect(res).to.haveGraphQLErrors( + WorkspaceJoinRequestNotFoundError.defaultMessage + ) + }) + it('should dismiss a workspace', async () => { + const workspace: BasicTestWorkspace = { + id: '', + slug: '', + ownerId: '', + name: cryptoRandomString({ length: 6 }), + description: cryptoRandomString({ length: 12 }) + } + await createTestWorkspace(workspace, testAdminUser) + + await db(WorkspaceJoinRequests.name).insert({ + workspaceId: workspace.id, + userId: testAdminUser.id, + status: 'pending' + }) + + const dismissRes = await apollo.execute(DismissWorkspaceDocument, { + input: { + workspaceId: workspace.id + } + }) + + expect(dismissRes).to.not.haveGraphQLErrors() + expect(dismissRes?.data?.workspaceMutations.dismiss).to.equal(true) + }) + }) + describe('mutation activeUserMutations.userWorkspaceMutations', () => { describe('leave', () => { it('allows the active user to leave a workspace', async () => { diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 4d3c4b2bd..8b418fe54 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4279,6 +4279,10 @@ export type WorkspaceCreationStateInput = { workspaceId: Scalars['ID']['input']; }; +export type WorkspaceDismissInput = { + workspaceId: Scalars['ID']['input']; +}; + export type WorkspaceDomain = { __typename?: 'WorkspaceDomain'; domain: Scalars['String']['output']; @@ -4372,6 +4376,7 @@ export type WorkspaceMutations = { delete: Scalars['Boolean']['output']; deleteDomain: Workspace; deleteSsoProvider: Scalars['Boolean']['output']; + dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; join: Workspace; leave: Scalars['Boolean']['output']; @@ -4409,6 +4414,11 @@ export type WorkspaceMutationsDeleteSsoProviderArgs = { }; +export type WorkspaceMutationsDismissArgs = { + input: WorkspaceDismissInput; +}; + + export type WorkspaceMutationsJoinArgs = { input: JoinWorkspaceInput; }; @@ -4861,6 +4871,13 @@ export type OnWorkspaceUpdatedSubscriptionVariables = Exact<{ export type OnWorkspaceUpdatedSubscription = { __typename?: 'Subscription', workspaceUpdated: { __typename?: 'WorkspaceUpdatedMessage', id: string, workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null, readOnly: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', totalCount: number, items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', id: string, name: string } }> }, invitedTeam?: Array<{ __typename?: 'PendingWorkspaceCollaborator', id: string, inviteId: string, workspaceId: string, workspaceName: string, title: string, role: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name: string }, user?: { __typename?: 'LimitedUser', id: string, name: string } | null }> | null } } }; +export type DismissWorkspaceMutationVariables = Exact<{ + input: WorkspaceDismissInput; +}>; + + +export type DismissWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', dismiss: boolean } }; + export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name: string } }; export type CreateStreamAccessRequestMutationVariables = Exact<{ @@ -5564,6 +5581,7 @@ export const GetWorkspaceDefaultRegionDocument = {"kind":"Document","definitions export const SetWorkspaceDefaultRegionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetWorkspaceDefaultRegion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"regionKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setDefaultRegion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"regionKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"regionKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRegion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const OnWorkspaceProjectsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnWorkspaceProjectsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceProjectsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const OnWorkspaceUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnWorkspaceUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode; +export const DismissWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"dismissWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceDismissInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dismiss"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode; export const CreateStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const GetStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const GetFullStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFullStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"stream"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; From ee5797af280db04676a18643e05042a7a7613905 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Fri, 10 Jan 2025 18:21:14 +0100 Subject: [PATCH 5/6] feat(workspaces): fix dismiss and tests --- .../modules/workspaces/errors/workspace.ts | 6 ----- .../workspaces/graph/resolvers/workspaces.ts | 1 + .../repositories/workspaceJoinRequests.ts | 6 +++-- .../services/workspaceJoinRequests.ts | 18 ++++++++++----- ....spec.ts => workspaceJoinRequests.spec.ts} | 23 ++++++++++++------- .../integration/workspaces.graph.spec.ts | 13 ++--------- 6 files changed, 34 insertions(+), 33 deletions(-) rename packages/server/modules/workspaces/tests/integration/{workspaceJoinReqquests.spec.ts => workspaceJoinRequests.spec.ts} (73%) diff --git a/packages/server/modules/workspaces/errors/workspace.ts b/packages/server/modules/workspaces/errors/workspace.ts index 19d2b012b..5a81b76b1 100644 --- a/packages/server/modules/workspaces/errors/workspace.ts +++ b/packages/server/modules/workspaces/errors/workspace.ts @@ -119,9 +119,3 @@ export class WorkspacePaidPlanActiveError extends BaseError { static code = 'WORKSPACE_PAID_PLAN_ACTIVE' static statusCode = 400 } - -export class WorkspaceJoinRequestNotFoundError extends BaseError { - static defaultMessage = 'Joinable workspace not found' - static code = 'WORKSPACE_JOIN_REQUEST_NOT_FOUND_ERROR' - static statusCode = 404 -} diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index da269a6f4..3717918ab 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -779,6 +779,7 @@ export = FF_WORKSPACES_MODULE_ENABLED projects: () => ({}), dismiss: async (_parent, args, ctx) => { return await dismissWorkspaceJoinRequestFactory({ + getWorkspace: getWorkspaceFactory({ db }), updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({ db }) diff --git a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts index a8a178231..a9ab04ca5 100644 --- a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts @@ -13,7 +13,9 @@ export const updateWorkspaceJoinRequestStatusFactory = async ({ workspaceId, userId, status }) => { const [request] = await tables .workspaceJoinRequests(db) - .update({ status }, ['workspaceId', 'userId']) - .where({ workspaceId, userId }) + .insert({ workspaceId, userId, status }) + .onConflict(['workspaceId', 'userId']) + .merge(['status']) + .returning('*') return request } diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts index a94fa5bf7..f758bc091 100644 --- a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -1,20 +1,26 @@ -import { UpdateWorkspaceJoinRequestStatus } from '@/modules/workspaces/domain/operations' -import { WorkspaceJoinRequestNotFoundError } from '@/modules/workspaces/errors/workspace' +import { + GetWorkspace, + UpdateWorkspaceJoinRequestStatus +} from '@/modules/workspaces/domain/operations' +import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' export const dismissWorkspaceJoinRequestFactory = ({ + getWorkspace, updateWorkspaceJoinRequestStatus }: { + getWorkspace: GetWorkspace updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus }) => async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { - const updated = await updateWorkspaceJoinRequestStatus({ + const workspace = await getWorkspace({ workspaceId }) + if (!workspace) { + throw new WorkspaceNotFoundError() + } + await updateWorkspaceJoinRequestStatus({ userId, workspaceId, status: 'dismissed' }) - if (!updated) { - throw new WorkspaceJoinRequestNotFoundError() - } return true } diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts similarity index 73% rename from packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts rename to packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts index ac2335486..2f8b3c598 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinReqquests.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts @@ -1,7 +1,9 @@ import { db } from '@/db/knex' import { createRandomString } from '@/modules/core/helpers/testHelpers' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' +import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' import { updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' +import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' import { dismissWorkspaceJoinRequestFactory } from '@/modules/workspaces/services/workspaceJoinRequests' import { BasicTestWorkspace, @@ -20,16 +22,18 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() 'workspaceJoinRequests services', () => { describe('dismissWorkspaceJoinRequestFactory, returns a function that ', () => { - it('throws a WorkspaceJoinRequestNotFoundError if the updateWorkspaceJoinRequestStatus does not exists', async () => { - expectToThrow(() => + it('throws an error if the workspace does not exists', async () => { + const err = await expectToThrow(() => dismissWorkspaceJoinRequestFactory({ + getWorkspace: getWorkspaceFactory({ db }), updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({ db }) })({ workspaceId: createRandomString(), userId: createRandomString() }) ) + expect(err.message).to.equal(WorkspaceNotFoundError.defaultMessage) }) - it('marks the request as dismissed', async () => { + it('creates the request with the dismissed status', async () => { const user: BasicTestUser = { id: '', name: 'John Speckle', @@ -49,18 +53,21 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() } await createTestWorkspace(workspace, user) - await db(WorkspaceJoinRequests.name).insert({ - workspaceId: workspace.id, - userId: user.id, - status: 'pending' - }) expect( await dismissWorkspaceJoinRequestFactory({ + getWorkspace: getWorkspaceFactory({ db }), updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({ db }) })({ workspaceId: workspace.id, userId: user.id }) ).to.equal(true) + + expect( + await db(WorkspaceJoinRequests.name) + .where({ workspaceId: workspace.id, userId: user.id }) + .select('status') + .first() + ).to.deep.equal({ status: 'dismissed' }) }) }) } diff --git a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts index 38cd06d30..261665f2c 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts @@ -48,8 +48,7 @@ import { } from '@/modules/core/helpers/testHelpers' import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' -import { WorkspaceJoinRequestNotFoundError } from '@/modules/workspaces/errors/workspace' -import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' +import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' const grantStreamPermissions = grantStreamPermissionsFactory({ db }) @@ -845,9 +844,7 @@ describe('Workspaces GQL CRUD', () => { workspaceId: cryptoRandomString({ length: 6 }) } }) - expect(res).to.haveGraphQLErrors( - WorkspaceJoinRequestNotFoundError.defaultMessage - ) + expect(res).to.haveGraphQLErrors(WorkspaceNotFoundError.defaultMessage) }) it('should dismiss a workspace', async () => { const workspace: BasicTestWorkspace = { @@ -859,12 +856,6 @@ describe('Workspaces GQL CRUD', () => { } await createTestWorkspace(workspace, testAdminUser) - await db(WorkspaceJoinRequests.name).insert({ - workspaceId: workspace.id, - userId: testAdminUser.id, - status: 'pending' - }) - const dismissRes = await apollo.execute(DismissWorkspaceDocument, { input: { workspaceId: workspace.id From f8cd33cce15504a472ec91b95fb346990ad28f8f Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Mon, 13 Jan 2025 19:21:14 +0100 Subject: [PATCH 6/6] feat(workspaces): add description to dismiss mutation --- .../server/assets/workspacesCore/typedefs/workspaces.graphql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql index 2eb3d1a3b..8114359be 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql @@ -151,6 +151,9 @@ type WorkspaceMutations { invites: WorkspaceInviteMutations! projects: WorkspaceProjectMutations! @hasServerRole(role: SERVER_USER) updateCreationState(input: WorkspaceCreationStateInput!): Boolean! + """ + Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" + """ dismiss(input: WorkspaceDismissInput!): Boolean! @hasServerRole(role: SERVER_USER) }