feat(server): send invitation review notification (#10418)

This commit is contained in:
fengmk2
2025-03-24 03:32:24 +00:00
parent dfd633b8b0
commit 28f8639aff
16 changed files with 629 additions and 137 deletions

View File

@@ -0,0 +1,11 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "NotificationType" ADD VALUE 'InvitationReviewRequest';
ALTER TYPE "NotificationType" ADD VALUE 'InvitationReviewApproved';
ALTER TYPE "NotificationType" ADD VALUE 'InvitationReviewDeclined';

View File

@@ -714,6 +714,9 @@ enum NotificationType {
InvitationAccepted
InvitationBlocked
InvitationRejected
InvitationReviewRequest
InvitationReviewApproved
InvitationReviewDeclined
}
enum NotificationLevel {

View File

@@ -494,6 +494,18 @@ test('should be able to approve team member', async t => {
t.is(memberInvite.status, 'UnderReview', 'should be under review');
t.true(await approveMember(app, tws.id, member.id));
const requestApprovedNotification = app.queue.last(
'notification.sendInvitationReviewApproved'
);
t.truthy(requestApprovedNotification);
t.deepEqual(
requestApprovedNotification.payload,
{
inviteId: memberInvite.inviteId,
reviewerId: owner.id,
},
'should send review approved notification'
);
}
{
@@ -627,7 +639,7 @@ test('should be able to invite batch and send notifications', async t => {
t.truthy(job.payload.inviterId);
});
test('should be able to emit events', async t => {
test('should be able to emit events and send notifications', async t => {
const { app, event } = t.context;
{
@@ -654,24 +666,35 @@ test('should be able to emit events', async t => {
app.switchUser(owner);
const { members } = await getWorkspace(app, tws.id);
const memberInvite = members.find(m => m.id === user.id)!;
const requestRequestNotification = app.queue.last(
'notification.sendInvitationReviewRequest'
);
t.truthy(requestRequestNotification);
// find admin
const admins = await t.context.models.workspaceUser.getAdmins(tws.id);
t.deepEqual(
event.emit.lastCall.args,
[
'workspace.members.reviewRequested',
{ inviteId: memberInvite.inviteId },
],
'should emit review requested event'
requestRequestNotification.payload,
{
inviteId: memberInvite.inviteId,
reviewerId: admins[0].id,
},
'should send review request notification'
);
app.switchUser(owner);
await revokeUser(app, tws.id, user.id);
const requestDeclinedNotification = app.queue.last(
'notification.sendInvitationReviewDeclined'
);
t.truthy(requestDeclinedNotification);
t.deepEqual(
event.emit.lastCall.args,
[
'workspace.members.requestDeclined',
{ userId: user.id, workspaceId: tws.id },
],
'should emit review requested event'
requestDeclinedNotification.payload,
{
userId: user.id,
workspaceId: tws.id,
reviewerId: owner.id,
},
'should send review declined notification'
);
}

View File

@@ -114,3 +114,83 @@ test('should ignore send invitation accepted notification when inviteId not exis
});
t.is(spy.callCount, 0);
});
test('should create invitation review request notification', async t => {
const { notificationJob, notificationService, models } = t.context;
const invite = await models.workspaceUser.set(
workspace.id,
member.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending
);
const spy = Sinon.spy(notificationService, 'createInvitationReviewRequest');
await notificationJob.sendInvitationReviewRequest({
reviewerId: owner.id,
inviteId: invite.id,
});
t.is(spy.callCount, 1);
t.is(spy.firstCall.args[0].userId, owner.id);
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
t.is(spy.firstCall.args[0].body.createdByUserId, member.id);
t.is(spy.firstCall.args[0].body.inviteId, invite.id);
});
test('should ignore send invitation review request notification when inviteId not exists', async t => {
const { notificationJob, notificationService } = t.context;
const spy = Sinon.spy(notificationService, 'createInvitationReviewRequest');
await notificationJob.sendInvitationReviewRequest({
reviewerId: owner.id,
inviteId: `not-exists-${randomUUID()}`,
});
t.is(spy.callCount, 0);
});
test('should create invitation review approved notification', async t => {
const { notificationJob, notificationService, models } = t.context;
const invite = await models.workspaceUser.set(
workspace.id,
member.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending
);
const spy = Sinon.spy(notificationService, 'createInvitationReviewApproved');
await notificationJob.sendInvitationReviewApproved({
reviewerId: owner.id,
inviteId: invite.id,
});
t.is(spy.callCount, 1);
t.is(spy.firstCall.args[0].userId, member.id);
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
t.is(spy.firstCall.args[0].body.createdByUserId, owner.id);
t.is(spy.firstCall.args[0].body.inviteId, invite.id);
});
test('should ignore send invitation review approved notification when inviteId not exists', async t => {
const { notificationJob, notificationService } = t.context;
const spy = Sinon.spy(notificationService, 'createInvitationReviewApproved');
await notificationJob.sendInvitationReviewApproved({
reviewerId: owner.id,
inviteId: `not-exists-${randomUUID()}`,
});
t.is(spy.callCount, 0);
});
test('should create invitation review declined notification', async t => {
const { notificationJob, notificationService, models } = t.context;
await models.workspaceUser.set(
workspace.id,
member.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending
);
const spy = Sinon.spy(notificationService, 'createInvitationReviewDeclined');
await notificationJob.sendInvitationReviewDeclined({
reviewerId: owner.id,
userId: member.id,
workspaceId: workspace.id,
});
t.is(spy.callCount, 1);
t.is(spy.firstCall.args[0].userId, member.id);
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
t.is(spy.firstCall.args[0].body.createdByUserId, owner.id);
});

View File

@@ -224,6 +224,139 @@ test('should create invitation rejected notification', async t => {
t.is(notification!.body.inviteId, inviteId);
});
test('should create invitation review request notification if user is not an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationReviewRequest({
userId: owner.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
inviteId,
},
});
t.truthy(notification);
t.is(notification!.type, NotificationType.InvitationReviewRequest);
t.is(notification!.userId, owner.id);
t.is(notification!.body.workspaceId, workspace.id);
t.is(notification!.body.createdByUserId, member.id);
t.is(notification!.body.inviteId, inviteId);
// should send email
const invitationReviewRequestMail = t.context.module.mails.last(
'LinkInvitationReviewRequest'
);
t.is(invitationReviewRequestMail.to, owner.email);
});
test('should not create invitation review request notification if user is an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const notification = await notificationService.createInvitationReviewRequest({
userId: owner.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
inviteId,
},
});
t.is(notification, undefined);
});
test('should create invitation review approved notification if user is an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const notification = await notificationService.createInvitationReviewApproved(
{
userId: member.id,
body: {
workspaceId: workspace.id,
createdByUserId: owner.id,
inviteId,
},
}
);
t.truthy(notification);
t.is(notification!.type, NotificationType.InvitationReviewApproved);
t.is(notification!.userId, member.id);
t.is(notification!.body.workspaceId, workspace.id);
t.is(notification!.body.createdByUserId, owner.id);
t.is(notification!.body.inviteId, inviteId);
// should send email
const invitationReviewApprovedMail = t.context.module.mails.last(
'LinkInvitationApprove'
);
t.is(invitationReviewApprovedMail.to, member.email);
});
test('should not create invitation review approved notification if user is not an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationReviewApproved(
{
userId: owner.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
inviteId,
},
}
);
t.is(notification, undefined);
});
test('should create invitation review declined notification if user is not an active member', async t => {
const { notificationService, models } = t.context;
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationReviewDeclined(
{
userId: member.id,
body: {
workspaceId: workspace.id,
createdByUserId: owner.id,
},
}
);
t.truthy(notification);
t.is(notification!.type, NotificationType.InvitationReviewDeclined);
t.is(notification!.userId, member.id);
t.is(notification!.body.workspaceId, workspace.id);
t.is(notification!.body.createdByUserId, owner.id);
// should send email
const invitationReviewDeclinedMail = t.context.module.mails.last(
'LinkInvitationDecline'
);
t.is(invitationReviewDeclinedMail.to, member.email);
});
test('should not create invitation review declined notification if user is an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const notification = await notificationService.createInvitationReviewDeclined(
{
userId: owner.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
},
}
);
t.is(notification, undefined);
});
test('should clean expired notifications', async t => {
const { notificationService } = t.context;
await notificationService.createInvitation({

View File

@@ -16,6 +16,19 @@ declare global {
inviterId: string;
inviteId: string;
};
'notification.sendInvitationReviewRequest': {
reviewerId: string;
inviteId: string;
};
'notification.sendInvitationReviewApproved': {
reviewerId: string;
inviteId: string;
};
'notification.sendInvitationReviewDeclined': {
reviewerId: string;
userId: string;
workspaceId: string;
};
}
}
@@ -80,4 +93,57 @@ export class NotificationJob {
},
});
}
@OnJob('notification.sendInvitationReviewRequest')
async sendInvitationReviewRequest({
reviewerId,
inviteId,
}: Jobs['notification.sendInvitationReviewRequest']) {
const invite = await this.models.workspaceUser.getById(inviteId);
if (!invite) {
return;
}
await this.service.createInvitationReviewRequest({
userId: reviewerId,
body: {
workspaceId: invite.workspaceId,
createdByUserId: invite.userId,
inviteId,
},
});
}
@OnJob('notification.sendInvitationReviewApproved')
async sendInvitationReviewApproved({
reviewerId,
inviteId,
}: Jobs['notification.sendInvitationReviewApproved']) {
const invite = await this.models.workspaceUser.getById(inviteId);
if (!invite) {
return;
}
await this.service.createInvitationReviewApproved({
userId: invite.userId,
body: {
workspaceId: invite.workspaceId,
createdByUserId: reviewerId,
inviteId,
},
});
}
@OnJob('notification.sendInvitationReviewDeclined')
async sendInvitationReviewDeclined({
reviewerId,
userId,
workspaceId,
}: Jobs['notification.sendInvitationReviewDeclined']) {
await this.service.createInvitationReviewDeclined({
userId,
body: {
workspaceId,
createdByUserId: reviewerId,
},
});
}
}

View File

@@ -10,6 +10,7 @@ import {
import {
DEFAULT_WORKSPACE_NAME,
InvitationNotificationCreate,
InvitationReviewDeclinedNotificationCreate,
MentionNotification,
MentionNotificationCreate,
Models,
@@ -191,6 +192,126 @@ export class NotificationService {
);
}
async createInvitationReviewRequest(input: InvitationNotificationCreate) {
const workspaceId = input.body.workspaceId;
const userId = input.userId;
if (await this.isActiveWorkspaceUser(workspaceId, userId)) {
return;
}
await this.ensureWorkspaceContentExists(workspaceId);
const notification = await this.models.notification.createInvitation(
input,
NotificationType.InvitationReviewRequest
);
await this.sendInvitationReviewRequestEmail(input);
return notification;
}
private async sendInvitationReviewRequestEmail(
input: InvitationNotificationCreate
) {
const inviteeUserId = input.body.createdByUserId;
const reviewerUserId = input.userId;
const workspaceId = input.body.workspaceId;
const reviewer = await this.models.user.getWorkspaceUser(reviewerUserId);
if (!reviewer) {
return;
}
await this.mailer.send({
name: 'LinkInvitationReviewRequest',
to: reviewer.email,
props: {
user: {
$$userId: inviteeUserId,
},
workspace: {
$$workspaceId: workspaceId,
},
url: this.url.link(`/workspace/${workspaceId}`),
},
});
this.logger.log(
`Invitation review request email sent to user ${reviewer.id} for workspace ${workspaceId}`
);
}
async createInvitationReviewApproved(input: InvitationNotificationCreate) {
const workspaceId = input.body.workspaceId;
const userId = input.userId;
if (!(await this.isActiveWorkspaceUser(workspaceId, userId))) {
return;
}
await this.ensureWorkspaceContentExists(workspaceId);
const notification = await this.models.notification.createInvitation(
input,
NotificationType.InvitationReviewApproved
);
await this.sendInvitationReviewApprovedEmail(input);
return notification;
}
private async sendInvitationReviewApprovedEmail(
input: InvitationNotificationCreate
) {
const workspaceId = input.body.workspaceId;
const receiverUserId = input.userId;
const receiver = await this.models.user.getWorkspaceUser(receiverUserId);
if (!receiver) {
return;
}
await this.mailer.send({
name: 'LinkInvitationApprove',
to: receiver.email,
props: {
workspace: {
$$workspaceId: workspaceId,
},
url: this.url.link(`/workspace/${workspaceId}`),
},
});
this.logger.log(
`Invitation review approved email sent to user ${receiver.id} for workspace ${workspaceId}`
);
}
async createInvitationReviewDeclined(
input: InvitationReviewDeclinedNotificationCreate
) {
const workspaceId = input.body.workspaceId;
const userId = input.userId;
if (await this.isActiveWorkspaceUser(workspaceId, userId)) {
return;
}
await this.ensureWorkspaceContentExists(workspaceId);
const notification =
await this.models.notification.createInvitationReviewDeclined(input);
await this.sendInvitationReviewDeclinedEmail(input);
return notification;
}
private async sendInvitationReviewDeclinedEmail(
input: InvitationReviewDeclinedNotificationCreate
) {
const workspaceId = input.body.workspaceId;
const receiverUserId = input.userId;
const receiver = await this.models.user.getWorkspaceUser(receiverUserId);
if (!receiver) {
return;
}
await this.mailer.send({
name: 'LinkInvitationDecline',
to: receiver.email,
props: {
workspace: {
$$workspaceId: workspaceId,
},
},
});
this.logger.log(
`Invitation review declined email sent to user ${receiver.id} for workspace ${workspaceId}`
);
}
private async ensureWorkspaceContentExists(workspaceId: string) {
await this.docReader.getWorkspaceContent(workspaceId);
}

View File

@@ -100,31 +100,38 @@ export class MentionNotificationBodyType extends BaseNotificationBodyType {
}
@ObjectType()
export class InvitationNotificationBodyType
extends BaseNotificationBodyType
implements Partial<InvitationNotificationBody>
{
export abstract class InvitationBaseNotificationBodyType extends BaseNotificationBodyType {
@Field(() => ID)
inviteId!: string;
}
@ObjectType()
export class InvitationNotificationBodyType
extends InvitationBaseNotificationBodyType
implements Partial<InvitationNotificationBody> {}
@ObjectType()
export class InvitationAcceptedNotificationBodyType
extends BaseNotificationBodyType
implements Partial<InvitationNotificationBody>
{
@Field(() => String)
inviteId!: string;
}
extends InvitationBaseNotificationBodyType
implements Partial<InvitationNotificationBody> {}
@ObjectType()
export class InvitationBlockedNotificationBodyType
extends BaseNotificationBodyType
implements Partial<InvitationNotificationBody>
{
@Field(() => String)
inviteId!: string;
}
extends InvitationBaseNotificationBodyType
implements Partial<InvitationNotificationBody> {}
@ObjectType()
export class InvitationReviewRequestNotificationBodyType
extends InvitationBaseNotificationBodyType
implements Partial<InvitationNotificationBody> {}
@ObjectType()
export class InvitationReviewApprovedNotificationBodyType
extends InvitationBaseNotificationBodyType
implements Partial<InvitationNotificationBody> {}
@ObjectType()
export class InvitationReviewDeclinedNotificationBodyType extends BaseNotificationBodyType {}
export const UnionNotificationBodyType = createUnionType({
name: 'UnionNotificationBodyType',
@@ -134,6 +141,9 @@ export const UnionNotificationBodyType = createUnionType({
InvitationNotificationBodyType,
InvitationAcceptedNotificationBodyType,
InvitationBlockedNotificationBodyType,
InvitationReviewRequestNotificationBodyType,
InvitationReviewApprovedNotificationBodyType,
InvitationReviewDeclinedNotificationBodyType,
] as const,
});

View File

@@ -11,38 +11,6 @@ export class WorkspaceEvents {
private readonly models: Models
) {}
@OnEvent('workspace.members.reviewRequested')
async onReviewRequested({
inviteId,
}: Events['workspace.members.reviewRequested']) {
// send review request mail to owner and admin
await this.workspaceService.sendReviewRequestedEmail(inviteId);
}
@OnEvent('workspace.members.requestApproved')
async onApproveRequest({
inviteId,
}: Events['workspace.members.requestApproved']) {
// send approve mail
await this.workspaceService.sendReviewApproveEmail(inviteId);
}
@OnEvent('workspace.members.requestDeclined')
async onDeclineRequest({
userId,
workspaceId,
}: Events['workspace.members.requestDeclined']) {
const user = await this.models.user.getWorkspaceUser(userId);
if (!user) {
return;
}
// send decline mail
await this.workspaceService.sendReviewDeclinedEmail(
user.email,
workspaceId
);
}
@OnEvent('workspace.members.roleChanged')
async onRoleChanged({
userId,

View File

@@ -148,7 +148,7 @@ export class WorkspaceService {
);
}
async sendReviewRequestedEmail(inviteId: string) {
async sendReviewRequestNotification(inviteId: string) {
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
if (!inviteeUserId) {
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
@@ -159,59 +159,31 @@ export class WorkspaceService {
const admins = await this.models.workspaceUser.getAdmins(workspaceId);
await Promise.allSettled(
[owner, ...admins].map(async receiver => {
await this.mailer.send({
name: 'LinkInvitationReviewRequest',
to: receiver.email,
props: {
user: {
$$userId: inviteeUserId,
},
workspace: {
$$workspaceId: workspaceId,
},
url: this.url.link(`/workspace/${workspaceId}`),
},
[owner, ...admins].map(async reviewer => {
await this.queue.add('notification.sendInvitationReviewRequest', {
reviewerId: reviewer.id,
inviteId,
});
})
);
}
async sendReviewApproveEmail(inviteId: string) {
const invitation = await this.models.workspaceUser.getById(inviteId);
if (!invitation) {
this.logger.warn(`Invitation not found for inviteId: ${inviteId}`);
return;
}
const user = await this.models.user.getWorkspaceUser(invitation.userId);
if (!user) {
this.logger.warn(`Invitee user not found for inviteId: ${inviteId}`);
return;
}
await this.mailer.send({
name: 'LinkInvitationApprove',
to: user.email,
props: {
workspace: {
$$workspaceId: invitation.workspaceId,
},
url: this.url.link(`/workspace/${invitation.workspaceId}`),
},
async sendReviewApprovedNotification(inviteId: string, reviewerId: string) {
await this.queue.add('notification.sendInvitationReviewApproved', {
reviewerId,
inviteId,
});
}
async sendReviewDeclinedEmail(email: string, workspaceId: string) {
await this.mailer.send({
name: 'LinkInvitationDecline',
to: email,
props: {
workspace: {
$$workspaceId: workspaceId,
},
},
async sendReviewDeclinedNotification(
userId: string,
workspaceId: string,
reviewerId: string
) {
await this.queue.add('notification.sendInvitationReviewDeclined', {
reviewerId,
userId,
workspaceId,
});
}

View File

@@ -223,12 +223,12 @@ export class TeamWorkspaceResolver {
@Mutation(() => Boolean)
async approveMember(
@CurrentUser() user: CurrentUser,
@CurrentUser() me: CurrentUser,
@Args('workspaceId') workspaceId: string,
@Args('userId') userId: string
) {
await this.ac
.user(user.id)
.user(me.id)
.workspace(workspaceId)
.assert('Workspace.Users.Manage');
@@ -242,9 +242,10 @@ export class TeamWorkspaceResolver {
WorkspaceMemberStatus.Accepted
);
this.event.emit('workspace.members.requestApproved', {
inviteId: result.id,
});
await this.workspaceService.sendReviewApprovedNotification(
result.id,
me.id
);
}
return true;
} else {

View File

@@ -529,11 +529,11 @@ export class WorkspaceResolver {
@Mutation(() => Boolean)
async revoke(
@CurrentUser() user: CurrentUser,
@CurrentUser() me: CurrentUser,
@Args('workspaceId') workspaceId: string,
@Args('userId') userId: string
) {
if (userId === user.id) {
if (userId === me.id) {
throw new CanNotRevokeYourself();
}
@@ -544,7 +544,7 @@ export class WorkspaceResolver {
}
await this.ac
.user(user.id)
.user(me.id)
.workspace(workspaceId)
.assert(
role.type === WorkspaceRole.Admin
@@ -562,10 +562,11 @@ export class WorkspaceResolver {
});
if (role.status === WorkspaceMemberStatus.UnderReview) {
this.event.emit('workspace.members.requestDeclined', {
await this.workspaceService.sendReviewDeclinedNotification(
userId,
workspaceId,
});
me.id
);
} else if (role.status === WorkspaceMemberStatus.Accepted) {
this.event.emit('workspace.members.removed', {
userId,
@@ -617,9 +618,7 @@ export class WorkspaceResolver {
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.UnderReview
);
this.event.emit('workspace.members.reviewRequested', {
inviteId: invite.id,
});
await this.workspaceService.sendReviewRequestNotification(invite.id);
return true;
} else {
const isTeam =

View File

@@ -78,9 +78,28 @@ export type InvitationNotificationCreate = z.input<
typeof InvitationNotificationCreateSchema
>;
const InvitationReviewDeclinedNotificationBodySchema = z.object({
workspaceId: IdSchema,
createdByUserId: IdSchema,
});
export type InvitationReviewDeclinedNotificationBody = z.infer<
typeof InvitationReviewDeclinedNotificationBodySchema
>;
export const InvitationReviewDeclinedNotificationCreateSchema =
BaseNotificationCreateSchema.extend({
body: InvitationReviewDeclinedNotificationBodySchema,
});
export type InvitationReviewDeclinedNotificationCreate = z.input<
typeof InvitationReviewDeclinedNotificationCreateSchema
>;
export type UnionNotificationBody =
| MentionNotificationBody
| InvitationNotificationBody;
| InvitationNotificationBody
| InvitationReviewDeclinedNotificationBody;
// #endregion
@@ -92,7 +111,13 @@ export type MentionNotification = Notification &
export type InvitationNotification = Notification &
z.infer<typeof InvitationNotificationCreateSchema>;
export type UnionNotification = MentionNotification | InvitationNotification;
export type InvitationReviewDeclinedNotification = Notification &
z.infer<typeof InvitationReviewDeclinedNotificationCreateSchema>;
export type UnionNotification =
| MentionNotification
| InvitationNotification
| InvitationReviewDeclinedNotification;
// #endregion
@@ -135,6 +160,23 @@ export class NotificationModel extends BaseModel {
return row as InvitationNotification;
}
async createInvitationReviewDeclined(
input: InvitationReviewDeclinedNotificationCreate
) {
const data = InvitationReviewDeclinedNotificationCreateSchema.parse(input);
const type = NotificationType.InvitationReviewDeclined;
const row = await this.create({
userId: data.userId,
level: data.level,
type,
body: data.body,
});
this.logger.log(
`Created ${type} notification ${row.id} to user ${data.userId} in workspace ${data.body.workspaceId}`
);
return row as InvitationReviewDeclinedNotification;
}
// #endregion
// #region common

View File

@@ -26,16 +26,6 @@ declare global {
workspaceId: string;
count: number;
};
'workspace.members.reviewRequested': {
inviteId: string;
};
'workspace.members.requestApproved': {
inviteId: string;
};
'workspace.members.requestDeclined': {
userId: string;
workspaceId: string;
};
'workspace.members.removed': {
userId: string;
workspaceId: string;

View File

@@ -624,7 +624,7 @@ type InvitationAcceptedNotificationBodyType {
The user who created the notification, maybe null when user is deleted or sent by system
"""
createdByUser: PublicUserType
inviteId: String!
inviteId: ID!
"""The type of the notification"""
type: NotificationType!
@@ -636,7 +636,7 @@ type InvitationBlockedNotificationBodyType {
The user who created the notification, maybe null when user is deleted or sent by system
"""
createdByUser: PublicUserType
inviteId: String!
inviteId: ID!
"""The type of the notification"""
type: NotificationType!
@@ -655,6 +655,41 @@ type InvitationNotificationBodyType {
workspace: NotificationWorkspaceType
}
type InvitationReviewApprovedNotificationBodyType {
"""
The user who created the notification, maybe null when user is deleted or sent by system
"""
createdByUser: PublicUserType
inviteId: ID!
"""The type of the notification"""
type: NotificationType!
workspace: NotificationWorkspaceType
}
type InvitationReviewDeclinedNotificationBodyType {
"""
The user who created the notification, maybe null when user is deleted or sent by system
"""
createdByUser: PublicUserType
"""The type of the notification"""
type: NotificationType!
workspace: NotificationWorkspaceType
}
type InvitationReviewRequestNotificationBodyType {
"""
The user who created the notification, maybe null when user is deleted or sent by system
"""
createdByUser: PublicUserType
inviteId: ID!
"""The type of the notification"""
type: NotificationType!
workspace: NotificationWorkspaceType
}
type InvitationType {
"""Invitee information"""
invitee: UserType!
@@ -1047,6 +1082,9 @@ enum NotificationType {
InvitationAccepted
InvitationBlocked
InvitationRejected
InvitationReviewApproved
InvitationReviewDeclined
InvitationReviewRequest
Mention
}
@@ -1435,7 +1473,7 @@ type TranscriptionResultType {
transcription: [TranscriptionItemType!]
}
union UnionNotificationBodyType = InvitationAcceptedNotificationBodyType | InvitationBlockedNotificationBodyType | InvitationNotificationBodyType | MentionNotificationBodyType
union UnionNotificationBodyType = InvitationAcceptedNotificationBodyType | InvitationBlockedNotificationBodyType | InvitationNotificationBodyType | InvitationReviewApprovedNotificationBodyType | InvitationReviewDeclinedNotificationBodyType | InvitationReviewRequestNotificationBodyType | MentionNotificationBodyType
type UnknownOauthProviderDataType {
name: String!