Merge pull request #3860 from specklesystems/alessandro/web-2495-create-id-resolver-and-add-user-to-workspace

Alessandro/web 2495 create id resolver and add user to workspace
This commit is contained in:
Alessandro Magionami
2025-01-22 16:15:51 +01:00
committed by GitHub
9 changed files with 202 additions and 66 deletions
@@ -468,6 +468,7 @@ type WorkspaceJoinRequestCollection {
}
type WorkspaceJoinRequest {
id: String!
workspace: Workspace!
user: LimitedUser!
status: WorkspaceJoinRequestStatus!
@@ -4423,6 +4423,7 @@ export type WorkspaceInviteUseInput = {
export type WorkspaceJoinRequest = {
__typename?: 'WorkspaceJoinRequest';
createdAt: Scalars['DateTime']['output'];
id: Scalars['String']['output'];
status: WorkspaceJoinRequestStatus;
user: LimitedUser;
workspace: Workspace;
@@ -6886,6 +6887,7 @@ export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, Pare
export type WorkspaceJoinRequestResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceJoinRequest'] = ResolversParentTypes['WorkspaceJoinRequest']> = {
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
status?: Resolver<ResolversTypes['WorkspaceJoinRequestStatus'], ParentType, ContextType>;
user?: Resolver<ResolversTypes['LimitedUser'], ParentType, ContextType>;
workspace?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType>;
@@ -4404,6 +4404,7 @@ export type WorkspaceInviteUseInput = {
export type WorkspaceJoinRequest = {
__typename?: 'WorkspaceJoinRequest';
createdAt: Scalars['DateTime']['output'];
id: Scalars['String']['output'];
status: WorkspaceJoinRequestStatus;
user: LimitedUser;
workspace: Workspace;
@@ -17,7 +17,10 @@ import {
getWorkspaceJoinRequestFactory,
updateWorkspaceJoinRequestStatusFactory
} from '@/modules/workspaces/repositories/workspaceJoinRequests'
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
import {
getWorkspaceFactory,
upsertWorkspaceRoleFactory
} from '@/modules/workspaces/repositories/workspaces'
import { sendWorkspaceJoinRequestApprovedEmailFactory } from '@/modules/workspaces/services/workspaceJoinRequestEmails/approved'
import { sendWorkspaceJoinRequestDeniedEmailFactory } from '@/modules/workspaces/services/workspaceJoinRequestEmails/denied'
import {
@@ -58,6 +61,9 @@ export default {
}
},
WorkspaceJoinRequest: {
id: async (parent) => {
return parent.userId + parent.workspaceId
},
user: async (parent, _args, ctx) => {
return await ctx.loaders.users.getUser.load(parent.userId)
},
@@ -91,7 +97,8 @@ export default {
getWorkspace: getWorkspaceFactory({ db }),
getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({
db
})
}),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db })
})
}
})
@@ -799,33 +799,35 @@ export = FF_WORKSPACES_MODULE_ENABLED
})({ userId: ctx.userId!, workspaceId: args.input.workspaceId })
},
requestToJoin: async (_parent, args, ctx) => {
const transaction = await db.transaction()
const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({
db: transaction
const requestToJoin = commandFactory({
db,
operationFactory: ({ db }) => {
const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({
db
})
const sendWorkspaceJoinRequestReceivedEmail =
sendWorkspaceJoinRequestReceivedEmailFactory({
renderEmail,
sendEmail,
getServerInfo,
getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({
db
}),
getUserEmails: findEmailsByUserIdFactory({ db })
})
return requestToJoinWorkspaceFactory({
createWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail,
getUserById: getUserFactory({ db }),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
getUserEmails: findEmailsByUserIdFactory({ db })
})
}
})
return await requestToJoin({
userId: ctx.userId!,
workspaceId: args.input.workspaceId
})
const sendWorkspaceJoinRequestReceivedEmail =
sendWorkspaceJoinRequestReceivedEmailFactory({
renderEmail,
sendEmail,
getServerInfo,
getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({
db: transaction
}),
getUserEmails: findEmailsByUserIdFactory({ db: transaction })
})
return await withTransaction(
requestToJoinWorkspaceFactory({
createWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail,
getUserById: getUserFactory({ db: transaction }),
getWorkspace: getWorkspaceFactory({ db: transaction })
})({
userId: ctx.userId!,
workspaceId: args.input.workspaceId
}),
transaction
)
}
},
WorkspaceInviteMutations: {
@@ -1,4 +1,9 @@
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import {
WorkspaceNotDiscoverableError,
WorkspaceNotFoundError,
WorkspaceNotJoinableError,
WorkspaceProtectedError
} from '@/modules/workspaces/errors/workspace'
import { GetUser } from '@/modules/core/domain/users/operations'
import { NotFoundError } from '@/modules/shared/errors'
import {
@@ -6,11 +11,16 @@ import {
DenyWorkspaceJoinRequest,
GetWorkspace,
GetWorkspaceJoinRequest,
GetWorkspaceWithDomains,
SendWorkspaceJoinRequestApprovedEmail,
SendWorkspaceJoinRequestDeniedEmail,
SendWorkspaceJoinRequestReceivedEmail,
UpdateWorkspaceJoinRequestStatus
UpdateWorkspaceJoinRequestStatus,
UpsertWorkspaceRole
} from '@/modules/workspaces/domain/operations'
import { Roles } from '@speckle/shared'
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
export const dismissWorkspaceJoinRequestFactory =
({
@@ -38,12 +48,14 @@ export const requestToJoinWorkspaceFactory =
createWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail,
getUserById,
getWorkspace
getWorkspaceWithDomains,
getUserEmails
}: {
createWorkspaceJoinRequest: CreateWorkspaceJoinRequest
sendWorkspaceJoinRequestReceivedEmail: SendWorkspaceJoinRequestReceivedEmail
getUserById: GetUser
getWorkspace: GetWorkspace
getWorkspaceWithDomains: GetWorkspaceWithDomains
getUserEmails: FindEmailsByUserId
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
const requester = await getUserById(userId)
@@ -51,10 +63,23 @@ export const requestToJoinWorkspaceFactory =
throw new NotFoundError('User not found')
}
const workspace = await getWorkspace({ workspaceId })
const workspace = await getWorkspaceWithDomains({ id: workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError('Workspace not found')
}
if (!workspace?.discoverabilityEnabled) throw new WorkspaceNotDiscoverableError()
const workspaceDomains = workspace.domains.filter((domain) => domain.verified)
if (!workspaceDomains.length) throw new WorkspaceNotJoinableError()
const userEmails = await getUserEmails({ userId })
const canJoinWorkspace = userEmailsCompliantWithWorkspaceDomains({
workspaceDomains: workspace.domains,
userEmails
})
if (!canJoinWorkspace) {
throw new WorkspaceProtectedError()
}
await createWorkspaceJoinRequest({
workspaceJoinRequest: {
@@ -78,13 +103,15 @@ export const approveWorkspaceJoinRequestFactory =
sendWorkspaceJoinRequestApprovedEmail,
getUserById,
getWorkspace,
getWorkspaceJoinRequest
getWorkspaceJoinRequest,
upsertWorkspaceRole
}: {
updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus
sendWorkspaceJoinRequestApprovedEmail: SendWorkspaceJoinRequestApprovedEmail
getUserById: GetUser
getWorkspace: GetWorkspace
getWorkspaceJoinRequest: GetWorkspaceJoinRequest
upsertWorkspaceRole: UpsertWorkspaceRole
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
const requester = await getUserById(userId)
@@ -112,6 +139,9 @@ export const approveWorkspaceJoinRequestFactory =
status: 'approved'
})
const role = Roles.Workspace.Member
await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() })
await sendWorkspaceJoinRequestApprovedEmail({
workspace,
requester
@@ -38,43 +38,59 @@ describe('WorkspaceJoinRequests GQL', () => {
it('should return the workspace join requests for the admin', async () => {
const admin = await createTestUser({
name: 'admin user',
role: Roles.Server.User
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const user1 = await createTestUser({ name: 'user 1', role: Roles.Server.User })
const user2 = await createTestUser({ name: 'user 2', role: Roles.Server.User })
const user1 = await createTestUser({
name: 'user 1',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const user2 = await createTestUser({
name: 'user 2',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const workspace1 = {
id: createRandomString(),
name: 'Workspace 1',
ownerId: admin.id,
description: ''
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspace1, admin)
await createTestWorkspace(workspace1, admin, { domain: 'example.org' })
const workspace2 = {
id: createRandomString(),
name: 'Workspace 2',
ownerId: admin.id,
description: ''
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspace2, admin)
await createTestWorkspace(workspace2, admin, { domain: 'example.org' })
const nobodyWorkspace = {
id: createRandomString(),
name: 'nobody',
ownerId: admin.id,
description: ''
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(nobodyWorkspace, admin)
await createTestWorkspace(nobodyWorkspace, admin, { domain: 'example.org' })
const nonAdminWorkspace = {
id: createRandomString(),
name: 'nonadmin',
ownerId: admin.id,
description: ''
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(nonAdminWorkspace, admin)
await createTestWorkspace(nonAdminWorkspace, admin, { domain: 'example.org' })
await upsertWorkspaceRoleFactory({ db })({
userId: admin.id,
workspaceId: nonAdminWorkspace.id,
@@ -4,7 +4,10 @@ import {
createRandomString
} from '@/modules/core/helpers/testHelpers'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import {
WorkspaceNotDiscoverableError,
WorkspaceNotFoundError
} from '@/modules/workspaces/errors/workspace'
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
import { UserWithOptionalRole } from '@/modules/core/repositories/users'
import {
@@ -12,7 +15,8 @@ import {
SendWorkspaceJoinRequestApprovedEmail,
SendWorkspaceJoinRequestDeniedEmail,
SendWorkspaceJoinRequestReceivedEmail,
UpdateWorkspaceJoinRequestStatus
UpdateWorkspaceJoinRequestStatus,
UpsertWorkspaceRole
} from '@/modules/workspaces/domain/operations'
import {
denyWorkspaceJoinRequestFactory,
@@ -24,7 +28,11 @@ import {
BasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { Workspace, WorkspaceJoinRequest } from '@/modules/workspacesCore/domain/types'
import {
Workspace,
WorkspaceJoinRequest,
WorkspaceWithDomains
} from '@/modules/workspacesCore/domain/types'
import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db'
import { expectToThrow } from '@/test/assertionHelper'
import { BasicTestUser, createTestUser } from '@/test/authHelper'
@@ -35,6 +43,7 @@ import {
createWorkspaceJoinRequestFactory,
updateWorkspaceJoinRequestStatusFactory
} from '@/modules/workspaces/repositories/workspaceJoinRequests'
import { UserEmail } from '@/modules/core/domain/userEmails/types'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
@@ -99,7 +108,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
Promise.resolve()) as unknown as CreateWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(),
getUserById: async () => null,
getWorkspace: async () => null
getWorkspaceWithDomains: async () => null,
getUserEmails: async () => []
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -113,21 +123,14 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
Promise.resolve()) as unknown as CreateWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(),
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => null
getWorkspaceWithDomains: async () => null,
getUserEmails: async () => []
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
expect(err.message).to.equal(WorkspaceNotFoundError.defaultMessage)
})
it('creates a join request and sends an email to all admins', async () => {
const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ db })
const sendWorkspaceJoinRequestReceivedEmailCalls: Parameters<SendWorkspaceJoinRequestReceivedEmail>[number][] =
[]
const sendWorkspaceJoinRequestReceivedEmail = async (
args: Parameters<SendWorkspaceJoinRequestReceivedEmail>[number]
) => sendWorkspaceJoinRequestReceivedEmailCalls.push(args)
it('throws a WorkspaceNotDiscoverable if the workspace has no domain', async () => {
const user: BasicTestUser = {
id: '',
name: 'John Speckle',
@@ -146,6 +149,57 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, user)
const err = await expectToThrow(() =>
requestToJoinWorkspaceFactory({
createWorkspaceJoinRequest: (async () =>
Promise.resolve()) as unknown as CreateWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(),
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspaceWithDomains: async () =>
workspace as unknown as WorkspaceWithDomains,
getUserEmails: async () => []
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
expect(err.message).to.equal(WorkspaceNotDiscoverableError.defaultMessage)
})
it('creates a join request and sends an email to all admins', async () => {
const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ db })
const sendWorkspaceJoinRequestReceivedEmailCalls: Parameters<SendWorkspaceJoinRequestReceivedEmail>[number][] =
[]
const sendWorkspaceJoinRequestReceivedEmail = async (
args: Parameters<SendWorkspaceJoinRequestReceivedEmail>[number]
) => sendWorkspaceJoinRequestReceivedEmailCalls.push(args)
const user: BasicTestUser = {
id: '',
name: 'John Speckle',
email: `${createRandomString()}@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 }),
discoverabilityEnabled: true
}
await createTestWorkspace(workspace, user, { domain: 'example.org' })
const domain = {
id: createRandomString(),
workspaceId: workspace.id,
domain: 'example.org',
verified: true,
createdAt: new Date(),
createdByUserId: user.id,
updatedAt: new Date()
}
expect(
await requestToJoinWorkspaceFactory({
@@ -153,7 +207,13 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestReceivedEmail:
sendWorkspaceJoinRequestReceivedEmail as unknown as SendWorkspaceJoinRequestReceivedEmail,
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace
getWorkspaceWithDomains: async () =>
({
...workspace,
domains: [domain]
} as unknown as WorkspaceWithDomains),
getUserEmails: async () =>
[{ email: user.email, verified: true }] as unknown as UserEmail[]
})({ workspaceId: workspace.id, userId: user.id })
).to.equal(true)
@@ -168,8 +228,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
).to.equal('pending')
expect(sendWorkspaceJoinRequestReceivedEmailCalls).to.have.length(1)
expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].workspace).to.equal(
workspace
expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].workspace.id).to.equal(
workspace.id
)
expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].requester).to.equal(user)
})
@@ -184,7 +244,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestApprovedEmail: async () => Promise.resolve(),
getUserById: async () => null,
getWorkspace: async () => null,
getWorkspaceJoinRequest: async () => undefined
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -199,7 +260,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestApprovedEmail: async () => Promise.resolve(),
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => null,
getWorkspaceJoinRequest: async () => undefined
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -222,7 +284,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestApprovedEmail: async () => Promise.resolve(),
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace,
getWorkspaceJoinRequest: async () => undefined
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -235,6 +298,13 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
args: Parameters<SendWorkspaceJoinRequestApprovedEmail>[number]
) => sendWorkspaceJoinRequestApprovedEmailCalls.push(args)
const upsertWorkspaceRoleCalls: Parameters<UpsertWorkspaceRole>[number][] = []
const upsertWorkspaceRole = async (
args: Parameters<UpsertWorkspaceRole>[number]
) => {
upsertWorkspaceRoleCalls.push(args)
}
const user: BasicTestUser = {
id: '',
name: 'John Speckle',
@@ -272,7 +342,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestApprovedEmail as unknown as SendWorkspaceJoinRequestApprovedEmail,
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace,
getWorkspaceJoinRequest: async () => request
getWorkspaceJoinRequest: async () => request,
upsertWorkspaceRole
})({ workspaceId: workspace.id, userId: user.id })
).to.equal(true)
@@ -286,6 +357,11 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
.first())!.status
).to.equal('approved')
expect(upsertWorkspaceRoleCalls).to.have.length(1)
expect(upsertWorkspaceRoleCalls[0].workspaceId).to.equal(workspace.id)
expect(upsertWorkspaceRoleCalls[0].userId).to.equal(user.id)
expect(upsertWorkspaceRoleCalls[0].role).to.equal(Roles.Workspace.Member)
expect(sendWorkspaceJoinRequestApprovedEmailCalls).to.have.length(1)
expect(sendWorkspaceJoinRequestApprovedEmailCalls[0].workspace).to.equal(
workspace
@@ -4405,6 +4405,7 @@ export type WorkspaceInviteUseInput = {
export type WorkspaceJoinRequest = {
__typename?: 'WorkspaceJoinRequest';
createdAt: Scalars['DateTime']['output'];
id: Scalars['String']['output'];
status: WorkspaceJoinRequestStatus;
user: LimitedUser;
workspace: Workspace;