feat(server): add invitation status to getInviteInfo response (#11158)
close CLOUD-182
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
acceptInviteByInviteIdMutation,
|
acceptInviteByInviteIdMutation,
|
||||||
|
createInviteLinkMutation,
|
||||||
|
getInviteInfoQuery,
|
||||||
getMembersByWorkspaceIdQuery,
|
getMembersByWorkspaceIdQuery,
|
||||||
inviteByEmailMutation,
|
inviteByEmailMutation,
|
||||||
leaveWorkspaceMutation,
|
leaveWorkspaceMutation,
|
||||||
revokeMemberPermissionMutation,
|
revokeMemberPermissionMutation,
|
||||||
|
WorkspaceInviteLinkExpireTime,
|
||||||
WorkspaceMemberStatus,
|
WorkspaceMemberStatus,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
@@ -32,6 +35,34 @@ e2e('should invite a user', async t => {
|
|||||||
const invitationNotification = app.queue.last('notification.sendInvitation');
|
const invitationNotification = app.queue.last('notification.sendInvitation');
|
||||||
t.is(invitationNotification.payload.inviterId, owner.id);
|
t.is(invitationNotification.payload.inviterId, owner.id);
|
||||||
t.is(invitationNotification.payload.inviteId, result.invite);
|
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 => {
|
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, {
|
const workspace = await app.create(Mockers.Workspace, {
|
||||||
owner: { id: owner.id },
|
owner: { id: owner.id },
|
||||||
});
|
});
|
||||||
await Promise.allSettled(
|
await Promise.all(
|
||||||
Array.from({ length: 10 }).map(async () => {
|
Array.from({ length: 10 }).map(async () => {
|
||||||
const user = await app.signup();
|
const user = await app.signup();
|
||||||
await app.create(Mockers.WorkspaceUser, {
|
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.memberCount, 11);
|
||||||
t.is(result.workspace.members.length, 10);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -544,27 +544,34 @@ test('should be able to invite by link', async t => {
|
|||||||
const [teamInviteId, teamInvite, acceptTeamInvite] =
|
const [teamInviteId, teamInvite, acceptTeamInvite] =
|
||||||
await createInviteLink(tws);
|
await createInviteLink(tws);
|
||||||
|
|
||||||
|
const member = await app.signup();
|
||||||
{
|
{
|
||||||
// check invite link
|
// check invite link
|
||||||
app.switchUser(owner);
|
app.switchUser(member);
|
||||||
const info = await getInviteInfo(app, inviteId);
|
const info = await getInviteInfo(app, inviteId);
|
||||||
t.is(info.workspace.id, ws.id, 'should be able to get invite info');
|
t.is(info.workspace.id, ws.id, 'should be able to get invite info');
|
||||||
|
t.falsy(info.status);
|
||||||
|
|
||||||
// check team invite link
|
// check team invite link
|
||||||
const teamInfo = await getInviteInfo(app, teamInviteId);
|
const teamInfo = await getInviteInfo(app, teamInviteId);
|
||||||
t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info');
|
t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info');
|
||||||
|
t.falsy(info.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// invite link
|
// invite link
|
||||||
for (const [i] of Array.from({ length: 5 }).entries()) {
|
for (const [i] of Array.from({ length: 5 }).entries()) {
|
||||||
const user = await invite(`test${i}@affine.pro`);
|
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(
|
t.is(
|
||||||
status,
|
status,
|
||||||
WorkspaceMemberStatus.UnderReview,
|
WorkspaceMemberStatus.UnderReview,
|
||||||
'should be able to check status'
|
'should be able to check status'
|
||||||
);
|
);
|
||||||
|
const info = await getInviteInfo(app, role!.id);
|
||||||
|
t.is(info.status, WorkspaceMemberStatus.UnderReview);
|
||||||
}
|
}
|
||||||
|
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export async function getInviteInfo(
|
|||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
|
status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { WorkspaceRole } from '../../permission';
|
|||||||
import { WorkspaceBlobStorage } from '../../storage';
|
import { WorkspaceBlobStorage } from '../../storage';
|
||||||
|
|
||||||
export type InviteInfo = {
|
export type InviteInfo = {
|
||||||
|
isLink: boolean;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
inviterUserId?: string;
|
inviterUserId?: string;
|
||||||
inviteeUserId?: string;
|
inviteeUserId?: string;
|
||||||
@@ -45,7 +46,10 @@ export class WorkspaceService {
|
|||||||
`workspace:inviteLinkId:${inviteId}`
|
`workspace:inviteLinkId:${inviteId}`
|
||||||
);
|
);
|
||||||
if (typeof invite?.workspaceId === 'string') {
|
if (typeof invite?.workspaceId === 'string') {
|
||||||
return invite;
|
return {
|
||||||
|
...invite,
|
||||||
|
isLink: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceUser = await this.models.workspaceUser.getById(inviteId);
|
const workspaceUser = await this.models.workspaceUser.getById(inviteId);
|
||||||
@@ -55,6 +59,7 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isLink: false,
|
||||||
workspaceId: workspaceUser.workspaceId,
|
workspaceId: workspaceUser.workspaceId,
|
||||||
inviteeUserId: workspaceUser.userId,
|
inviteeUserId: workspaceUser.userId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -514,8 +514,8 @@ export class WorkspaceResolver {
|
|||||||
async getInviteInfo(
|
async getInviteInfo(
|
||||||
@CurrentUser() user: UserType | undefined,
|
@CurrentUser() user: UserType | undefined,
|
||||||
@Args('inviteId') inviteId: string
|
@Args('inviteId') inviteId: string
|
||||||
) {
|
): Promise<InvitationType> {
|
||||||
const { workspaceId, inviteeUserId } =
|
const { workspaceId, inviteeUserId, isLink } =
|
||||||
await this.workspaceService.getInviteInfo(inviteId);
|
await this.workspaceService.getInviteInfo(inviteId);
|
||||||
const workspace = await this.workspaceService.getWorkspaceInfo(workspaceId);
|
const workspace = await this.workspaceService.getWorkspaceInfo(workspaceId);
|
||||||
const owner = await this.models.workspaceUser.getOwner(workspaceId);
|
const owner = await this.models.workspaceUser.getOwner(workspaceId);
|
||||||
@@ -523,8 +523,21 @@ export class WorkspaceResolver {
|
|||||||
const inviteeId = inviteeUserId || user?.id;
|
const inviteeId = inviteeUserId || user?.id;
|
||||||
if (!inviteeId) throw new UserNotFound();
|
if (!inviteeId) throw new UserNotFound();
|
||||||
const invitee = await this.models.user.getWorkspaceUser(inviteeId);
|
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)
|
@Mutation(() => Boolean)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { WorkspaceMemberStatus } from '@prisma/client';
|
|||||||
import { SafeIntResolver } from 'graphql-scalars';
|
import { SafeIntResolver } from 'graphql-scalars';
|
||||||
|
|
||||||
import { DocRole, WorkspaceRole } from '../permission';
|
import { DocRole, WorkspaceRole } from '../permission';
|
||||||
import { UserType } from '../user/types';
|
import { UserType, WorkspaceUserType } from '../user/types';
|
||||||
|
|
||||||
registerEnumType(WorkspaceRole, {
|
registerEnumType(WorkspaceRole, {
|
||||||
name: 'WorkspaceRole',
|
name: 'WorkspaceRole',
|
||||||
@@ -120,9 +120,14 @@ export class InvitationType {
|
|||||||
@Field({ description: 'Workspace information' })
|
@Field({ description: 'Workspace information' })
|
||||||
workspace!: InvitationWorkspaceType;
|
workspace!: InvitationWorkspaceType;
|
||||||
@Field({ description: 'User information' })
|
@Field({ description: 'User information' })
|
||||||
user!: UserType;
|
user!: WorkspaceUserType;
|
||||||
@Field({ description: 'Invitee information' })
|
@Field({ description: 'Invitee information' })
|
||||||
invitee!: UserType;
|
invitee!: WorkspaceUserType;
|
||||||
|
@Field(() => WorkspaceMemberStatus, {
|
||||||
|
description: 'Invitation status in workspace',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
status?: WorkspaceMemberStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
|
|||||||
@@ -692,10 +692,13 @@ type InvitationReviewRequestNotificationBodyType {
|
|||||||
|
|
||||||
type InvitationType {
|
type InvitationType {
|
||||||
"""Invitee information"""
|
"""Invitee information"""
|
||||||
invitee: UserType!
|
invitee: WorkspaceUserType!
|
||||||
|
|
||||||
|
"""Invitation status in workspace"""
|
||||||
|
status: WorkspaceMemberStatus
|
||||||
|
|
||||||
"""User information"""
|
"""User information"""
|
||||||
user: UserType!
|
user: WorkspaceUserType!
|
||||||
|
|
||||||
"""Workspace information"""
|
"""Workspace information"""
|
||||||
workspace: InvitationWorkspaceType!
|
workspace: InvitationWorkspaceType!
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ query getInviteInfo($inviteId: String!) {
|
|||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
|
status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -750,6 +750,7 @@ export const getInviteInfoQuery = {
|
|||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
|
status
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -834,9 +834,11 @@ export interface InvitationReviewRequestNotificationBodyType {
|
|||||||
export interface InvitationType {
|
export interface InvitationType {
|
||||||
__typename?: 'InvitationType';
|
__typename?: 'InvitationType';
|
||||||
/** Invitee information */
|
/** Invitee information */
|
||||||
invitee: UserType;
|
invitee: WorkspaceUserType;
|
||||||
|
/** Invitation status in workspace */
|
||||||
|
status: Maybe<WorkspaceMemberStatus>;
|
||||||
/** User information */
|
/** User information */
|
||||||
user: UserType;
|
user: WorkspaceUserType;
|
||||||
/** Workspace information */
|
/** Workspace information */
|
||||||
workspace: InvitationWorkspaceType;
|
workspace: InvitationWorkspaceType;
|
||||||
}
|
}
|
||||||
@@ -3192,6 +3194,7 @@ export type GetInviteInfoQuery = {
|
|||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
getInviteInfo: {
|
getInviteInfo: {
|
||||||
__typename?: 'InvitationType';
|
__typename?: 'InvitationType';
|
||||||
|
status: WorkspaceMemberStatus | null;
|
||||||
workspace: {
|
workspace: {
|
||||||
__typename?: 'InvitationWorkspaceType';
|
__typename?: 'InvitationWorkspaceType';
|
||||||
id: string;
|
id: string;
|
||||||
@@ -3199,7 +3202,7 @@ export type GetInviteInfoQuery = {
|
|||||||
avatar: string;
|
avatar: string;
|
||||||
};
|
};
|
||||||
user: {
|
user: {
|
||||||
__typename?: 'UserType';
|
__typename?: 'WorkspaceUserType';
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user