From 4d15c3224227cfc6eeee0d7666629e0694b3b413 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 25 Mar 2025 06:51:26 +0000 Subject: [PATCH] feat(server): add invitation status to getInviteInfo response (#11158) close CLOUD-182 --- .../__tests__/e2e/workspace/invite.spec.ts | 92 ++++++++++++++++++- .../backend/server/src/__tests__/team.e2e.ts | 11 ++- .../server/src/__tests__/utils/invite.ts | 1 + .../src/core/workspaces/resolvers/service.ts | 7 +- .../core/workspaces/resolvers/workspace.ts | 19 +++- .../server/src/core/workspaces/types.ts | 11 ++- packages/backend/server/src/schema.gql | 7 +- .../graphql/src/graphql/get-invite-info.gql | 1 + packages/common/graphql/src/graphql/index.ts | 1 + packages/common/graphql/src/schema.ts | 9 +- 10 files changed, 144 insertions(+), 15 deletions(-) diff --git a/packages/backend/server/src/__tests__/e2e/workspace/invite.spec.ts b/packages/backend/server/src/__tests__/e2e/workspace/invite.spec.ts index 45f84fa84c..4024df5436 100644 --- a/packages/backend/server/src/__tests__/e2e/workspace/invite.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/workspace/invite.spec.ts @@ -1,9 +1,12 @@ import { acceptInviteByInviteIdMutation, + createInviteLinkMutation, + getInviteInfoQuery, getMembersByWorkspaceIdQuery, inviteByEmailMutation, leaveWorkspaceMutation, revokeMemberPermissionMutation, + WorkspaceInviteLinkExpireTime, WorkspaceMemberStatus, } from '@affine/graphql'; import { faker } from '@faker-js/faker'; @@ -32,6 +35,34 @@ e2e('should invite a user', async t => { const invitationNotification = app.queue.last('notification.sendInvitation'); t.is(invitationNotification.payload.inviterId, owner.id); t.is(invitationNotification.payload.inviteId, result.invite); + + // invitation status is pending + const { getInviteInfo } = await app.gql({ + query: getInviteInfoQuery, + variables: { + inviteId: result.invite, + }, + }); + t.is(getInviteInfo.status, WorkspaceMemberStatus.Pending); + + // u2 accept invite + await app.switchUser(u2); + await app.gql({ + query: acceptInviteByInviteIdMutation, + variables: { + workspaceId: workspace.id, + inviteId: result.invite, + }, + }); + + // invitation status is accepted + const { getInviteInfo: getInviteInfo2 } = await app.gql({ + query: getInviteInfoQuery, + variables: { + inviteId: result.invite, + }, + }); + t.is(getInviteInfo2.status, WorkspaceMemberStatus.Accepted); }); e2e('should leave a workspace', async t => { @@ -290,7 +321,7 @@ e2e('should limit member count correctly', async t => { const workspace = await app.create(Mockers.Workspace, { owner: { id: owner.id }, }); - await Promise.allSettled( + await Promise.all( Array.from({ length: 10 }).map(async () => { const user = await app.signup(); await app.create(Mockers.WorkspaceUser, { @@ -312,3 +343,62 @@ e2e('should limit member count correctly', async t => { t.is(result.workspace.memberCount, 11); t.is(result.workspace.members.length, 10); }); + +e2e('should get invite link info with status', async t => { + const owner = await app.signup(); + + const workspace = await app.create(Mockers.Workspace, { + owner: { id: owner.id }, + }); + + await app.login(owner); + const { createInviteLink } = await app.gql({ + query: createInviteLinkMutation, + variables: { + workspaceId: workspace.id, + expireTime: WorkspaceInviteLinkExpireTime.OneDay, + }, + }); + t.truthy(createInviteLink, 'failed to create invite link'); + const link = createInviteLink.link; + const inviteId = link.split('/').pop()!; + + // owner/member see accept status + const { getInviteInfo } = await app.gql({ + query: getInviteInfoQuery, + variables: { + inviteId, + }, + }); + t.truthy(getInviteInfo, 'failed to get invite info'); + t.is(getInviteInfo.status, WorkspaceMemberStatus.Accepted); + + // non-member see null status + await app.signup(); + const { getInviteInfo: getInviteInfo2 } = await app.gql({ + query: getInviteInfoQuery, + variables: { + inviteId, + }, + }); + t.truthy(getInviteInfo2, 'failed to get invite info'); + t.is(getInviteInfo2.status, null); + + // pending-member see under review status + await app.signup(); + await app.gql({ + query: acceptInviteByInviteIdMutation, + variables: { + workspaceId: workspace.id, + inviteId, + }, + }); + const { getInviteInfo: getInviteInfo3 } = await app.gql({ + query: getInviteInfoQuery, + variables: { + inviteId, + }, + }); + t.truthy(getInviteInfo3, 'failed to get invite info'); + t.is(getInviteInfo3.status, WorkspaceMemberStatus.UnderReview); +}); diff --git a/packages/backend/server/src/__tests__/team.e2e.ts b/packages/backend/server/src/__tests__/team.e2e.ts index 42226820d1..6f0097c67b 100644 --- a/packages/backend/server/src/__tests__/team.e2e.ts +++ b/packages/backend/server/src/__tests__/team.e2e.ts @@ -544,27 +544,34 @@ test('should be able to invite by link', async t => { const [teamInviteId, teamInvite, acceptTeamInvite] = await createInviteLink(tws); + const member = await app.signup(); { // check invite link - app.switchUser(owner); + app.switchUser(member); const info = await getInviteInfo(app, inviteId); t.is(info.workspace.id, ws.id, 'should be able to get invite info'); + t.falsy(info.status); // check team invite link const teamInfo = await getInviteInfo(app, teamInviteId); t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info'); + t.falsy(info.status); } { // invite link for (const [i] of Array.from({ length: 5 }).entries()) { const user = await invite(`test${i}@affine.pro`); - const status = (await models.workspaceUser.get(ws.id, user.id))?.status; + const role = await models.workspaceUser.get(ws.id, user.id); + t.truthy(role); + const status = role!.status; t.is( status, WorkspaceMemberStatus.UnderReview, 'should be able to check status' ); + const info = await getInviteInfo(app, role!.id); + t.is(info.status, WorkspaceMemberStatus.UnderReview); } await t.throwsAsync( diff --git a/packages/backend/server/src/__tests__/utils/invite.ts b/packages/backend/server/src/__tests__/utils/invite.ts index 91878968ec..42add4be92 100644 --- a/packages/backend/server/src/__tests__/utils/invite.ts +++ b/packages/backend/server/src/__tests__/utils/invite.ts @@ -163,6 +163,7 @@ export async function getInviteInfo( name avatarUrl } + status } } `); diff --git a/packages/backend/server/src/core/workspaces/resolvers/service.ts b/packages/backend/server/src/core/workspaces/resolvers/service.ts index e89b60fec9..a596da99ee 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/service.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/service.ts @@ -20,6 +20,7 @@ import { WorkspaceRole } from '../../permission'; import { WorkspaceBlobStorage } from '../../storage'; export type InviteInfo = { + isLink: boolean; workspaceId: string; inviterUserId?: string; inviteeUserId?: string; @@ -45,7 +46,10 @@ export class WorkspaceService { `workspace:inviteLinkId:${inviteId}` ); if (typeof invite?.workspaceId === 'string') { - return invite; + return { + ...invite, + isLink: true, + }; } const workspaceUser = await this.models.workspaceUser.getById(inviteId); @@ -55,6 +59,7 @@ export class WorkspaceService { } return { + isLink: false, workspaceId: workspaceUser.workspaceId, inviteeUserId: workspaceUser.userId, }; diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts index 614c05b2f0..ddcd96ee6e 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts @@ -514,8 +514,8 @@ export class WorkspaceResolver { async getInviteInfo( @CurrentUser() user: UserType | undefined, @Args('inviteId') inviteId: string - ) { - const { workspaceId, inviteeUserId } = + ): Promise { + const { workspaceId, inviteeUserId, isLink } = await this.workspaceService.getInviteInfo(inviteId); const workspace = await this.workspaceService.getWorkspaceInfo(workspaceId); const owner = await this.models.workspaceUser.getOwner(workspaceId); @@ -523,8 +523,21 @@ export class WorkspaceResolver { const inviteeId = inviteeUserId || user?.id; if (!inviteeId) throw new UserNotFound(); const invitee = await this.models.user.getWorkspaceUser(inviteeId); + if (!invitee) throw new UserNotFound(); - return { workspace, user: owner, invitee }; + let status: WorkspaceMemberStatus | undefined; + if (isLink) { + const invitation = await this.models.workspaceUser.get( + workspaceId, + inviteeId + ); + status = invitation?.status; + } else { + const invitation = await this.models.workspaceUser.getById(inviteId); + status = invitation?.status; + } + + return { workspace, user: owner, invitee, status }; } @Mutation(() => Boolean) diff --git a/packages/backend/server/src/core/workspaces/types.ts b/packages/backend/server/src/core/workspaces/types.ts index 30c84099ff..0f3b74dde1 100644 --- a/packages/backend/server/src/core/workspaces/types.ts +++ b/packages/backend/server/src/core/workspaces/types.ts @@ -12,7 +12,7 @@ import { WorkspaceMemberStatus } from '@prisma/client'; import { SafeIntResolver } from 'graphql-scalars'; import { DocRole, WorkspaceRole } from '../permission'; -import { UserType } from '../user/types'; +import { UserType, WorkspaceUserType } from '../user/types'; registerEnumType(WorkspaceRole, { name: 'WorkspaceRole', @@ -120,9 +120,14 @@ export class InvitationType { @Field({ description: 'Workspace information' }) workspace!: InvitationWorkspaceType; @Field({ description: 'User information' }) - user!: UserType; + user!: WorkspaceUserType; @Field({ description: 'Invitee information' }) - invitee!: UserType; + invitee!: WorkspaceUserType; + @Field(() => WorkspaceMemberStatus, { + description: 'Invitation status in workspace', + nullable: true, + }) + status?: WorkspaceMemberStatus; } @InputType() diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index b7d624b4fb..97b2550dd4 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -692,10 +692,13 @@ type InvitationReviewRequestNotificationBodyType { type InvitationType { """Invitee information""" - invitee: UserType! + invitee: WorkspaceUserType! + + """Invitation status in workspace""" + status: WorkspaceMemberStatus """User information""" - user: UserType! + user: WorkspaceUserType! """Workspace information""" workspace: InvitationWorkspaceType! diff --git a/packages/common/graphql/src/graphql/get-invite-info.gql b/packages/common/graphql/src/graphql/get-invite-info.gql index 7cae4450c8..faee37e8df 100644 --- a/packages/common/graphql/src/graphql/get-invite-info.gql +++ b/packages/common/graphql/src/graphql/get-invite-info.gql @@ -10,5 +10,6 @@ query getInviteInfo($inviteId: String!) { name avatarUrl } + status } } diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index a7448b193b..f34fcc3b2b 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -750,6 +750,7 @@ export const getInviteInfoQuery = { name avatarUrl } + status } }`, }; diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index c651c28354..e9c5d55e58 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -834,9 +834,11 @@ export interface InvitationReviewRequestNotificationBodyType { export interface InvitationType { __typename?: 'InvitationType'; /** Invitee information */ - invitee: UserType; + invitee: WorkspaceUserType; + /** Invitation status in workspace */ + status: Maybe; /** User information */ - user: UserType; + user: WorkspaceUserType; /** Workspace information */ workspace: InvitationWorkspaceType; } @@ -3192,6 +3194,7 @@ export type GetInviteInfoQuery = { __typename?: 'Query'; getInviteInfo: { __typename?: 'InvitationType'; + status: WorkspaceMemberStatus | null; workspace: { __typename?: 'InvitationWorkspaceType'; id: string; @@ -3199,7 +3202,7 @@ export type GetInviteInfoQuery = { avatar: string; }; user: { - __typename?: 'UserType'; + __typename?: 'WorkspaceUserType'; id: string; name: string; avatarUrl: string | null;