diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts index efbeadc5a..50707fe69 100644 --- a/packages/server/modules/workspaces/events/eventListener.ts +++ b/packages/server/modules/workspaces/events/eventListener.ts @@ -6,7 +6,8 @@ import { import { getStream } from '@/modules/core/repositories/streams' import { GetWorkspaceRoles, - GetWorkspaceRoleToDefaultProjectRoleMapping + GetWorkspaceRoleToDefaultProjectRoleMapping, + QueryAllWorkspaceProjects } from '@/modules/workspaces/domain/operations' import { ServerInvitesEvents, @@ -20,8 +21,9 @@ import { logger } from '@/logging/logging' import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management' import { getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants' -import { Roles } from '@speckle/shared' +import { Roles, WorkspaceRoles } from '@speckle/shared' import { UpsertProjectRole } from '@/modules/core/domain/projects/operations' +import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' export const onProjectCreatedFactory = ({ @@ -107,19 +109,64 @@ export const onInviteFinalizedFactory = }) } +export const onWorkspaceJoinedFactory = + ({ + getDefaultWorkspaceProjectRoleMapping, + queryAllWorkspaceProjects, + upsertProjectRole + }: { + getDefaultWorkspaceProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping + queryAllWorkspaceProjects: QueryAllWorkspaceProjects + upsertProjectRole: UpsertProjectRole + }) => + async ({ + userId, + role, + workspaceId + }: { + userId: string + role: WorkspaceRoles + workspaceId: string + }) => { + const defaultRoleMapping = await getDefaultWorkspaceProjectRoleMapping({ + workspaceId + }) + + const maybeProjectRole = defaultRoleMapping[role] + if (!maybeProjectRole) return + + for await (const projects of queryAllWorkspaceProjects({ workspaceId })) { + await Promise.all( + projects.map(async (project) => { + await upsertProjectRole({ + projectId: project.id, + userId, + role: maybeProjectRole + }) + }) + ) + } + } + export const initializeEventListenersFactory = ({ onProjectCreated, - onInviteFinalized + onInviteFinalized, + onWorkspaceJoined }: { onProjectCreated: ReturnType onInviteFinalized: ReturnType + onWorkspaceJoined: ReturnType }) => () => { + const eventBus = getEventBus() const quitCbs = [ ProjectsEmitter.listen(ProjectEvents.Created, onProjectCreated), - getEventBus().listen(ServerInvitesEvents.Finalized, ({ payload }) => + eventBus.listen(ServerInvitesEvents.Finalized, ({ payload }) => onInviteFinalized(payload) + ), + eventBus.listen(WorkspaceEvents.JoinedFromDiscovery, ({ payload }) => + onWorkspaceJoined(payload) ) ] diff --git a/packages/server/modules/workspaces/index.ts b/packages/server/modules/workspaces/index.ts index d5cb8828e..43fb1e1ae 100644 --- a/packages/server/modules/workspaces/index.ts +++ b/packages/server/modules/workspaces/index.ts @@ -9,7 +9,8 @@ import { registerOrUpdateRole } from '@/modules/shared/repositories/roles' import { initializeEventListenersFactory, onInviteFinalizedFactory, - onProjectCreatedFactory + onProjectCreatedFactory, + onWorkspaceJoinedFactory } from '@/modules/workspaces/events/eventListener' import { getWorkspaceRolesFactory, @@ -63,6 +64,11 @@ const workspacesModule: SpeckleModule = { upsertProjectRole: upsertProjectRoleFactory({ db }), getWorkspaceRoles: getWorkspaceRolesFactory({ db }) }), + onWorkspaceJoined: onWorkspaceJoinedFactory({ + getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole, + queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }), + upsertProjectRole: upsertProjectRoleFactory({ db }) + }), onInviteFinalized: onInviteFinalizedFactory({ getStream, logger: moduleLogger, diff --git a/packages/server/modules/workspaces/services/join.ts b/packages/server/modules/workspaces/services/join.ts index a1aa2a661..30c756555 100644 --- a/packages/server/modules/workspaces/services/join.ts +++ b/packages/server/modules/workspaces/services/join.ts @@ -46,7 +46,7 @@ export const joinWorkspaceFactory = await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() }) await emitWorkspaceEvent({ eventName: WorkspaceEvents.JoinedFromDiscovery, - payload: { userId, workspaceId } + payload: { userId, workspaceId, role } }) await emitWorkspaceEvent({ eventName: WorkspaceEvents.RoleUpdated, diff --git a/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts b/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts index bd03b56c2..c8282a9a4 100644 --- a/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts @@ -1,10 +1,14 @@ import cryptoRandomString from 'crypto-random-string' import { WorkspaceAcl } from '@/modules/workspacesCore/domain/types' -import { Roles } from '@speckle/shared' +import { Roles, StreamRoles } from '@speckle/shared' import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types' -import { onProjectCreatedFactory } from '@/modules/workspaces/events/eventListener' +import { + onProjectCreatedFactory, + onWorkspaceJoinedFactory +} from '@/modules/workspaces/events/eventListener' import { expect } from 'chai' import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic' +import { chunk } from 'lodash' describe('Event handlers', () => { describe('onProjectCreatedFactory creates a function, that', () => { @@ -57,4 +61,60 @@ describe('Event handlers', () => { expect(projectRoles.length).to.equal(2) }) }) + describe('onWorkspaceJoinedFactory creates a function, that', () => { + it('assigns no project roles if the role mapping returns null', async () => { + await onWorkspaceJoinedFactory({ + getDefaultWorkspaceProjectRoleMapping: async () => ({ + [Roles.Workspace.Admin]: Roles.Stream.Owner, + [Roles.Workspace.Member]: Roles.Stream.Contributor, + [Roles.Workspace.Guest]: null + }), + async *queryAllWorkspaceProjects() { + expect.fail() + }, + upsertProjectRole: async () => { + expect.fail() + } + })({ + role: Roles.Workspace.Guest, + userId: cryptoRandomString({ length: 10 }), + workspaceId: cryptoRandomString({ length: 10 }) + }) + }) + it('assigns the mapped projects roles to all queried project', async () => { + const projectIds = [ + cryptoRandomString({ length: 10 }), + cryptoRandomString({ length: 10 }), + cryptoRandomString({ length: 10 }), + cryptoRandomString({ length: 10 }) + ] + const userId = cryptoRandomString({ length: 10 }) + const projectRole = Roles.Stream.Reviewer + + const storedRoles: { userId: string; role: StreamRoles; projectId: string }[] = [] + await onWorkspaceJoinedFactory({ + getDefaultWorkspaceProjectRoleMapping: async () => ({ + [Roles.Workspace.Admin]: Roles.Stream.Owner, + [Roles.Workspace.Member]: projectRole, + [Roles.Workspace.Guest]: null + }), + async *queryAllWorkspaceProjects() { + for (const projIds of chunk(projectIds, 3)) { + yield projIds.map((projId) => ({ id: projId } as unknown as StreamRecord)) + } + }, + upsertProjectRole: async (args) => { + storedRoles.push(args) + return {} as StreamRecord + } + })({ + role: Roles.Workspace.Member, + userId, + workspaceId: cryptoRandomString({ length: 10 }) + }) + expect(storedRoles).deep.equals( + projectIds.map((projectId) => ({ projectId, role: projectRole, userId })) + ) + }) + }) }) diff --git a/packages/server/modules/workspacesCore/domain/events.ts b/packages/server/modules/workspacesCore/domain/events.ts index 662c236b8..dd3147d10 100644 --- a/packages/server/modules/workspacesCore/domain/events.ts +++ b/packages/server/modules/workspacesCore/domain/events.ts @@ -1,4 +1,5 @@ import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types' +import { WorkspaceRoles } from '@speckle/shared' export const workspaceEventNamespace = 'workspace' as const @@ -20,7 +21,11 @@ type WorkspaceCreatedPayload = Workspace & { type WorkspaceUpdatedPayload = Workspace type WorkspaceRoleDeletedPayload = Pick type WorkspaceRoleUpdatedPayload = Pick -type WorkspaceJoinedFromDiscoveryPayload = { userId: string; workspaceId: string } +type WorkspaceJoinedFromDiscoveryPayload = { + userId: string + workspaceId: string + role: WorkspaceRoles +} export type WorkspaceEventsPayloads = { [WorkspaceEvents.Created]: WorkspaceCreatedPayload