mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
feat(server): support comment notification type (#12924)
#### PR Dependency Tree * **PR #12924** 👈 * **PR #12925** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced comment and comment mention notifications, including email notifications when users are mentioned or receive comments on documents. * Added new email templates for comment and comment mention notifications. * Users can now control whether they receive comment-related emails via a new user setting. * **Bug Fixes** * None. * **Documentation** * Updated GraphQL schema documentation to reflect new notification types and user settings. * **Refactor** * Streamlined and enhanced test coverage for notification and user settings, including comment notifications. * **Chores** * Improved test setup and snapshot coverage for user settings and notifications. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
-- 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 'Comment';
|
||||||
|
ALTER TYPE "NotificationType" ADD VALUE 'CommentMention';
|
||||||
@@ -822,6 +822,8 @@ enum NotificationType {
|
|||||||
InvitationReviewRequest
|
InvitationReviewRequest
|
||||||
InvitationReviewApproved
|
InvitationReviewApproved
|
||||||
InvitationReviewDeclined
|
InvitationReviewDeclined
|
||||||
|
Comment
|
||||||
|
CommentMention
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NotificationLevel {
|
enum NotificationLevel {
|
||||||
|
|||||||
@@ -1513,6 +1513,179 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
<!--/$-->␊
|
<!--/$-->␊
|
||||||
`
|
`
|
||||||
|
|
||||||
|
> test@test.com commented on Test Doc
|
||||||
|
|
||||||
|
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||||
|
<!--$-->␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody>␊
|
||||||
|
<tr>␊
|
||||||
|
<td>␊
|
||||||
|
<p␊
|
||||||
|
style="font-size:20px;line-height:28px;font-weight:600;font-family:Inter, Arial, Helvetica, sans-serif;margin-top:24px;margin-bottom:0;color:#141414">␊
|
||||||
|
You have a new comment␊
|
||||||
|
</p>␊
|
||||||
|
</td>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody>␊
|
||||||
|
<tr>␊
|
||||||
|
<td>␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody style="width:100%">␊
|
||||||
|
<tr style="width:100%">␊
|
||||||
|
<p␊
|
||||||
|
style="font-size:15px;line-height:24px;font-weight:400;font-family:Inter, Arial, Helvetica, sans-serif;margin-top:24px;margin-bottom:0;color:#141414">␊
|
||||||
|
<span style="font-weight:600">test@test.com</span> commented on␊
|
||||||
|
<a␊
|
||||||
|
href="https://app.affine.pro"␊
|
||||||
|
style="color:#067df7;text-decoration-line:none"␊
|
||||||
|
target="_blank"␊
|
||||||
|
><span style="font-weight:600">Test Doc</span></a␊
|
||||||
|
>.␊
|
||||||
|
</p>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody style="width:100%">␊
|
||||||
|
<tr style="width:100%">␊
|
||||||
|
<a␊
|
||||||
|
href="https://app.affine.pro"␊
|
||||||
|
style="line-height:24px;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;font-size:15px;font-weight:600;font-family:Inter, Arial, Helvetica, sans-serif;margin-top:24px;margin-bottom:0;color:#FFFFFF;background-color:#1E96EB;padding:8px 18px 8px 18px;border-radius:8px;border:1px solid rgba(0,0,0,.1);margin-right:4px"␊
|
||||||
|
target="_blank"␊
|
||||||
|
><span␊
|
||||||
|
><!--[if mso]><i style="mso-font-width:450%;mso-text-raise:12" hidden>  </i><![endif]--></span␊
|
||||||
|
><span␊
|
||||||
|
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:6px"␊
|
||||||
|
>View Comment</span␊
|
||||||
|
><span␊
|
||||||
|
><!--[if mso]><i style="mso-font-width:450%" hidden>  ​</i><![endif]--></span␊
|
||||||
|
></a␊
|
||||||
|
>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
</td>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
<!--/$-->␊
|
||||||
|
`
|
||||||
|
|
||||||
|
> test@test.com mentioned you in a comment on Test Doc
|
||||||
|
|
||||||
|
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||||
|
<!--$-->␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody>␊
|
||||||
|
<tr>␊
|
||||||
|
<td>␊
|
||||||
|
<p␊
|
||||||
|
style="font-size:20px;line-height:28px;font-weight:600;font-family:Inter, Arial, Helvetica, sans-serif;margin-top:24px;margin-bottom:0;color:#141414">␊
|
||||||
|
You are mentioned in a comment␊
|
||||||
|
</p>␊
|
||||||
|
</td>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody>␊
|
||||||
|
<tr>␊
|
||||||
|
<td>␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody style="width:100%">␊
|
||||||
|
<tr style="width:100%">␊
|
||||||
|
<p␊
|
||||||
|
style="font-size:15px;line-height:24px;font-weight:400;font-family:Inter, Arial, Helvetica, sans-serif;margin-top:24px;margin-bottom:0;color:#141414">␊
|
||||||
|
<span style="font-weight:600">test@test.com</span> mentioned you␊
|
||||||
|
in a comment on␊
|
||||||
|
<a␊
|
||||||
|
href="https://app.affine.pro"␊
|
||||||
|
style="color:#067df7;text-decoration-line:none"␊
|
||||||
|
target="_blank"␊
|
||||||
|
><span style="font-weight:600">Test Doc</span></a␊
|
||||||
|
>.␊
|
||||||
|
</p>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
<table␊
|
||||||
|
align="center"␊
|
||||||
|
width="100%"␊
|
||||||
|
border="0"␊
|
||||||
|
cellpadding="0"␊
|
||||||
|
cellspacing="0"␊
|
||||||
|
role="presentation">␊
|
||||||
|
<tbody style="width:100%">␊
|
||||||
|
<tr style="width:100%">␊
|
||||||
|
<a␊
|
||||||
|
href="https://app.affine.pro"␊
|
||||||
|
style="line-height:24px;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;font-size:15px;font-weight:600;font-family:Inter, Arial, Helvetica, sans-serif;margin-top:24px;margin-bottom:0;color:#FFFFFF;background-color:#1E96EB;padding:8px 18px 8px 18px;border-radius:8px;border:1px solid rgba(0,0,0,.1);margin-right:4px"␊
|
||||||
|
target="_blank"␊
|
||||||
|
><span␊
|
||||||
|
><!--[if mso]><i style="mso-font-width:450%;mso-text-raise:12" hidden>  </i><![endif]--></span␊
|
||||||
|
><span␊
|
||||||
|
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:6px"␊
|
||||||
|
>View Comment</span␊
|
||||||
|
><span␊
|
||||||
|
><!--[if mso]><i style="mso-font-width:450%" hidden>  ​</i><![endif]--></span␊
|
||||||
|
></a␊
|
||||||
|
>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
</td>␊
|
||||||
|
</tr>␊
|
||||||
|
</tbody>␊
|
||||||
|
</table>␊
|
||||||
|
<!--/$-->␊
|
||||||
|
`
|
||||||
|
|
||||||
> Your workspace has been upgraded to team workspace! 🎉
|
> Your workspace has been upgraded to team workspace! 🎉
|
||||||
|
|
||||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||||
|
|||||||
Binary file not shown.
@@ -44,7 +44,8 @@ export class MailSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get configured() {
|
get configured() {
|
||||||
return this.smtp !== null;
|
// NOTE: testing environment will use mock queue, so we need to return true
|
||||||
|
return this.smtp !== null || env.testing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('config.init')
|
@OnEvent('config.init')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
type TestingModule,
|
type TestingModule,
|
||||||
} from '../../../__tests__/utils';
|
} from '../../../__tests__/utils';
|
||||||
import {
|
import {
|
||||||
|
DocMode,
|
||||||
Models,
|
Models,
|
||||||
User,
|
User,
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -204,3 +205,24 @@ test('should create invitation review declined notification', async t => {
|
|||||||
t.is(spy.firstCall.args[0].body.workspaceId, workspace.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.createdByUserId, owner.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create comment notification', async t => {
|
||||||
|
const { notificationJob, notificationService } = t.context;
|
||||||
|
const spy = Sinon.spy(notificationService, 'createComment');
|
||||||
|
|
||||||
|
await notificationJob.sendComment({
|
||||||
|
userId: member.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: owner.id,
|
||||||
|
doc: {
|
||||||
|
id: randomUUID(),
|
||||||
|
title: 'doc-title-1',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
commentId: randomUUID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(spy.callCount, 1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { mock } from 'node:test';
|
import { mock } from 'node:test';
|
||||||
|
|
||||||
import ava, { TestFn } from 'ava';
|
import test from 'ava';
|
||||||
|
|
||||||
|
import { createModule } from '../../../__tests__/create-module';
|
||||||
import { Mockers } from '../../../__tests__/mocks';
|
import { Mockers } from '../../../__tests__/mocks';
|
||||||
import {
|
import { Due, NotificationNotFound } from '../../../base';
|
||||||
createTestingModule,
|
|
||||||
type TestingModule,
|
|
||||||
} from '../../../__tests__/utils';
|
|
||||||
import { NotificationNotFound } from '../../../base';
|
|
||||||
import {
|
import {
|
||||||
DocMode,
|
DocMode,
|
||||||
MentionNotificationBody,
|
MentionNotificationBody,
|
||||||
@@ -18,33 +15,33 @@ import {
|
|||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceMemberStatus,
|
WorkspaceMemberStatus,
|
||||||
} from '../../../models';
|
} from '../../../models';
|
||||||
import { DocReader } from '../../doc';
|
import { DocStorageModule } from '../../doc';
|
||||||
|
import { FeatureModule } from '../../features';
|
||||||
|
import { MailModule } from '../../mail';
|
||||||
|
import { PermissionModule } from '../../permission';
|
||||||
|
import { StorageModule } from '../../storage';
|
||||||
|
import { NotificationModule } from '..';
|
||||||
import { NotificationService } from '../service';
|
import { NotificationService } from '../service';
|
||||||
|
|
||||||
interface Context {
|
const module = await createModule({
|
||||||
module: TestingModule;
|
imports: [
|
||||||
notificationService: NotificationService;
|
FeatureModule,
|
||||||
models: Models;
|
PermissionModule,
|
||||||
docReader: DocReader;
|
DocStorageModule,
|
||||||
}
|
StorageModule,
|
||||||
|
MailModule,
|
||||||
const test = ava as TestFn<Context>;
|
NotificationModule,
|
||||||
|
],
|
||||||
test.before(async t => {
|
providers: [NotificationService],
|
||||||
const module = await createTestingModule();
|
|
||||||
t.context.module = module;
|
|
||||||
t.context.notificationService = module.get(NotificationService);
|
|
||||||
t.context.models = module.get(Models);
|
|
||||||
t.context.docReader = module.get(DocReader);
|
|
||||||
});
|
});
|
||||||
|
const notificationService = module.get(NotificationService);
|
||||||
|
const models = module.get(Models);
|
||||||
|
|
||||||
let owner: User;
|
let owner: User;
|
||||||
let member: User;
|
let member: User;
|
||||||
let workspace: Workspace;
|
let workspace: Workspace;
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async () => {
|
||||||
const { module } = t.context;
|
|
||||||
await module.initTestingDB();
|
|
||||||
owner = await module.create(Mockers.User);
|
owner = await module.create(Mockers.User);
|
||||||
member = await module.create(Mockers.User);
|
member = await module.create(Mockers.User);
|
||||||
workspace = await module.create(Mockers.Workspace, {
|
workspace = await module.create(Mockers.Workspace, {
|
||||||
@@ -61,13 +58,13 @@ test.afterEach.always(() => {
|
|||||||
mock.timers.reset();
|
mock.timers.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.after.always(async t => {
|
test.after.always(async () => {
|
||||||
await t.context.module.close();
|
await module.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation notification and email', async t => {
|
test('should create invitation notification and email', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
|
|
||||||
const notification = await notificationService.createInvitation({
|
const notification = await notificationService.createInvitation({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -76,25 +73,28 @@ test('should create invitation notification and email', async t => {
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.Invitation);
|
t.is(notification!.type, NotificationType.Invitation);
|
||||||
t.is(notification!.userId, member.id);
|
t.is(notification!.userId, member.id);
|
||||||
t.is(notification!.body.workspaceId, workspace.id);
|
t.is(notification!.body.workspaceId, workspace.id);
|
||||||
t.is(notification!.body.createdByUserId, owner.id);
|
t.is(notification!.body.createdByUserId, owner.id);
|
||||||
t.is(notification!.body.inviteId, inviteId);
|
t.is(notification!.body.inviteId, inviteId);
|
||||||
|
|
||||||
// should send invitation email
|
// should send invitation email
|
||||||
const invitationMail = t.context.module.mails.last('MemberInvitation');
|
const invitationMail = module.queue.last('notification.sendMail');
|
||||||
t.is(invitationMail.to, member.email);
|
t.is(invitationMail.payload.to, member.email);
|
||||||
|
t.is(invitationMail.payload.name, 'MemberInvitation');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not send invitation email if user setting is not to receive invitation email', async t => {
|
test('should not send invitation email if user setting is not to receive invitation email', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
await module.create(Mockers.UserSettings, {
|
await module.create(Mockers.UserSettings, {
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
receiveInvitationEmail: false,
|
receiveInvitationEmail: false,
|
||||||
});
|
});
|
||||||
const invitationMailCount = module.mails.count('MemberInvitation');
|
const invitationMailCount = module.queue.count('notification.sendMail');
|
||||||
|
|
||||||
const notification = await notificationService.createInvitation({
|
const notification = await notificationService.createInvitation({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -103,17 +103,19 @@ test('should not send invitation email if user setting is not to receive invitat
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
|
|
||||||
// no new invitation email should be sent
|
// no new invitation email should be sent
|
||||||
t.is(t.context.module.mails.count('MemberInvitation'), invitationMailCount);
|
t.is(module.queue.count('notification.sendMail'), invitationMailCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not create invitation notification if user is already a member', async t => {
|
test('should not create invitation notification if user is already a member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createInvitation({
|
const notification = await notificationService.createInvitation({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -122,15 +124,16 @@ test('should not create invitation notification if user is already a member', as
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.is(notification, undefined);
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation accepted notification and email', async t => {
|
test('should create invitation accepted notification and email', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationAccepted({
|
const notification = await notificationService.createInvitationAccepted({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -139,6 +142,7 @@ test('should create invitation accepted notification and email', async t => {
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.InvitationAccepted);
|
t.is(notification!.type, NotificationType.InvitationAccepted);
|
||||||
t.is(notification!.userId, owner.id);
|
t.is(notification!.userId, owner.id);
|
||||||
@@ -147,12 +151,12 @@ test('should create invitation accepted notification and email', async t => {
|
|||||||
t.is(notification!.body.inviteId, inviteId);
|
t.is(notification!.body.inviteId, inviteId);
|
||||||
|
|
||||||
// should send email
|
// should send email
|
||||||
const invitationAcceptedMail = module.mails.last('MemberAccepted');
|
const invitationAcceptedMail = module.queue.last('notification.sendMail');
|
||||||
t.is(invitationAcceptedMail.to, owner.email);
|
t.is(invitationAcceptedMail.payload.to, owner.email);
|
||||||
|
t.is(invitationAcceptedMail.payload.name, 'MemberAccepted');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not send invitation accepted email if user settings is not receive invitation email', async t => {
|
test('should not send invitation accepted email if user settings is not receive invitation email', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
@@ -162,8 +166,10 @@ test('should not send invitation accepted email if user settings is not receive
|
|||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
receiveInvitationEmail: false,
|
receiveInvitationEmail: false,
|
||||||
});
|
});
|
||||||
const invitationAcceptedMailCount =
|
const invitationAcceptedMailCount = module.queue.count(
|
||||||
t.context.module.mails.count('MemberAccepted');
|
'notification.sendMail'
|
||||||
|
);
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationAccepted({
|
const notification = await notificationService.createInvitationAccepted({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -172,17 +178,19 @@ test('should not send invitation accepted email if user settings is not receive
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
|
|
||||||
// no new invitation accepted email should be sent
|
// no new invitation accepted email should be sent
|
||||||
t.is(
|
t.is(
|
||||||
t.context.module.mails.count('MemberAccepted'),
|
module.queue.count('notification.sendMail'),
|
||||||
invitationAcceptedMailCount
|
invitationAcceptedMailCount
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not create invitation accepted notification if user is not an active member', async t => {
|
test('should not create invitation accepted notification if user is not an active member', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationAccepted({
|
const notification = await notificationService.createInvitationAccepted({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -191,12 +199,13 @@ test('should not create invitation accepted notification if user is not an activ
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.is(notification, undefined);
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation blocked notification', async t => {
|
test('should create invitation blocked notification', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationBlocked({
|
const notification = await notificationService.createInvitationBlocked({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -205,6 +214,7 @@ test('should create invitation blocked notification', async t => {
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.InvitationBlocked);
|
t.is(notification!.type, NotificationType.InvitationBlocked);
|
||||||
t.is(notification!.userId, owner.id);
|
t.is(notification!.userId, owner.id);
|
||||||
@@ -214,8 +224,8 @@ test('should create invitation blocked notification', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation rejected notification', async t => {
|
test('should create invitation rejected notification', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationRejected({
|
const notification = await notificationService.createInvitationRejected({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -224,6 +234,7 @@ test('should create invitation rejected notification', async t => {
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.InvitationRejected);
|
t.is(notification!.type, NotificationType.InvitationRejected);
|
||||||
t.is(notification!.userId, owner.id);
|
t.is(notification!.userId, owner.id);
|
||||||
@@ -233,8 +244,8 @@ test('should create invitation rejected notification', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation review request notification if user is not an active member', async t => {
|
test('should create invitation review request notification if user is not an active member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationReviewRequest({
|
const notification = await notificationService.createInvitationReviewRequest({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -243,6 +254,7 @@ test('should create invitation review request notification if user is not an act
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.InvitationReviewRequest);
|
t.is(notification!.type, NotificationType.InvitationReviewRequest);
|
||||||
t.is(notification!.userId, owner.id);
|
t.is(notification!.userId, owner.id);
|
||||||
@@ -251,18 +263,19 @@ test('should create invitation review request notification if user is not an act
|
|||||||
t.is(notification!.body.inviteId, inviteId);
|
t.is(notification!.body.inviteId, inviteId);
|
||||||
|
|
||||||
// should send email
|
// should send email
|
||||||
const invitationReviewRequestMail = module.mails.last(
|
const invitationReviewRequestMail = module.queue.last(
|
||||||
'LinkInvitationReviewRequest'
|
'notification.sendMail'
|
||||||
);
|
);
|
||||||
t.is(invitationReviewRequestMail.to, owner.email);
|
t.is(invitationReviewRequestMail.payload.to, owner.email);
|
||||||
|
t.is(invitationReviewRequestMail.payload.name, 'LinkInvitationReviewRequest');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not create invitation review request notification if user is an active member', async t => {
|
test('should not create invitation review request notification if user is an active member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationReviewRequest({
|
const notification = await notificationService.createInvitationReviewRequest({
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -271,15 +284,16 @@ test('should not create invitation review request notification if user is an act
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.is(notification, undefined);
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation review approved notification if user is an active member', async t => {
|
test('should create invitation review approved notification if user is an active member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationReviewApproved(
|
const notification = await notificationService.createInvitationReviewApproved(
|
||||||
{
|
{
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
@@ -290,6 +304,7 @@ test('should create invitation review approved notification if user is an active
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.InvitationReviewApproved);
|
t.is(notification!.type, NotificationType.InvitationReviewApproved);
|
||||||
t.is(notification!.userId, member.id);
|
t.is(notification!.userId, member.id);
|
||||||
@@ -298,19 +313,20 @@ test('should create invitation review approved notification if user is an active
|
|||||||
t.is(notification!.body.inviteId, inviteId);
|
t.is(notification!.body.inviteId, inviteId);
|
||||||
|
|
||||||
// should send email
|
// should send email
|
||||||
const invitationReviewApprovedMail = t.context.module.mails.last(
|
const invitationReviewApprovedMail = module.queue.last(
|
||||||
'LinkInvitationApprove'
|
'notification.sendMail'
|
||||||
);
|
);
|
||||||
t.is(invitationReviewApprovedMail.to, member.email);
|
t.is(invitationReviewApprovedMail.payload.to, member.email);
|
||||||
|
t.is(invitationReviewApprovedMail.payload.name, 'LinkInvitationApprove');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not create invitation review approved notification if user is not an active member', async t => {
|
test('should not create invitation review approved notification if user is not an active member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
status: WorkspaceMemberStatus.Pending,
|
status: WorkspaceMemberStatus.Pending,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationReviewApproved(
|
const notification = await notificationService.createInvitationReviewApproved(
|
||||||
{
|
{
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
@@ -321,11 +337,11 @@ test('should not create invitation review approved notification if user is not a
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
t.is(notification, undefined);
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation review declined notification if user is not an active member', async t => {
|
test('should create invitation review declined notification if user is not an active member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const notification = await notificationService.createInvitationReviewDeclined(
|
const notification = await notificationService.createInvitationReviewDeclined(
|
||||||
{
|
{
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
@@ -335,6 +351,7 @@ test('should create invitation review declined notification if user is not an ac
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
t.is(notification!.type, NotificationType.InvitationReviewDeclined);
|
t.is(notification!.type, NotificationType.InvitationReviewDeclined);
|
||||||
t.is(notification!.userId, member.id);
|
t.is(notification!.userId, member.id);
|
||||||
@@ -342,18 +359,19 @@ test('should create invitation review declined notification if user is not an ac
|
|||||||
t.is(notification!.body.createdByUserId, owner.id);
|
t.is(notification!.body.createdByUserId, owner.id);
|
||||||
|
|
||||||
// should send email
|
// should send email
|
||||||
const invitationReviewDeclinedMail = module.mails.last(
|
const invitationReviewDeclinedMail = module.queue.last(
|
||||||
'LinkInvitationDecline'
|
'notification.sendMail'
|
||||||
);
|
);
|
||||||
t.is(invitationReviewDeclinedMail.to, member.email);
|
t.is(invitationReviewDeclinedMail.payload.to, member.email);
|
||||||
|
t.is(invitationReviewDeclinedMail.payload.name, 'LinkInvitationDecline');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not create invitation review declined notification if user is an active member', async t => {
|
test('should not create invitation review declined notification if user is an active member', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
await module.create(Mockers.WorkspaceUser, {
|
await module.create(Mockers.WorkspaceUser, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createInvitationReviewDeclined(
|
const notification = await notificationService.createInvitationReviewDeclined(
|
||||||
{
|
{
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
@@ -363,11 +381,11 @@ test('should not create invitation review declined notification if user is an ac
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
t.is(notification, undefined);
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should clean expired notifications', async t => {
|
test('should clean expired notifications', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
await notificationService.createInvitation({
|
await notificationService.createInvitation({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -376,29 +394,35 @@ test('should clean expired notifications', async t => {
|
|||||||
inviteId: randomUUID(),
|
inviteId: randomUUID(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let count = await notificationService.countByUserId(member.id);
|
let count = await notificationService.countByUserId(member.id);
|
||||||
t.is(count, 1);
|
t.is(count, 1);
|
||||||
|
|
||||||
// wait for 100 days
|
// wait for 100 days
|
||||||
mock.timers.enable({
|
mock.timers.enable({
|
||||||
apis: ['Date'],
|
apis: ['Date'],
|
||||||
now: Date.now() + 1000 * 60 * 60 * 24 * 100,
|
now: Due.after('100d'),
|
||||||
});
|
});
|
||||||
await t.context.models.notification.cleanExpiredNotifications();
|
|
||||||
|
await models.notification.cleanExpiredNotifications();
|
||||||
|
|
||||||
count = await notificationService.countByUserId(member.id);
|
count = await notificationService.countByUserId(member.id);
|
||||||
t.is(count, 1);
|
t.is(count, 1);
|
||||||
|
|
||||||
mock.timers.reset();
|
mock.timers.reset();
|
||||||
// wait for 1 year
|
// wait for 1 year
|
||||||
mock.timers.enable({
|
mock.timers.enable({
|
||||||
apis: ['Date'],
|
apis: ['Date'],
|
||||||
now: Date.now() + 1000 * 60 * 60 * 24 * 365,
|
now: Due.after('1y'),
|
||||||
});
|
});
|
||||||
await t.context.models.notification.cleanExpiredNotifications();
|
|
||||||
|
await models.notification.cleanExpiredNotifications();
|
||||||
|
|
||||||
count = await notificationService.countByUserId(member.id);
|
count = await notificationService.countByUserId(member.id);
|
||||||
t.is(count, 0);
|
t.is(count, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should mark notification as read', async t => {
|
test('should mark notification as read', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
const notification = await notificationService.createInvitation({
|
const notification = await notificationService.createInvitation({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -407,22 +431,20 @@ test('should mark notification as read', async t => {
|
|||||||
inviteId: randomUUID(),
|
inviteId: randomUUID(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await notificationService.markAsRead(member.id, notification!.id);
|
await notificationService.markAsRead(member.id, notification!.id);
|
||||||
const updatedNotification = await t.context.models.notification.get(
|
|
||||||
notification!.id
|
const updatedNotification = await models.notification.get(notification!.id);
|
||||||
);
|
|
||||||
t.is(updatedNotification!.read, true);
|
t.is(updatedNotification!.read, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error on mark notification as read if notification is not found', async t => {
|
test('should throw error on mark notification as read if notification is not found', async t => {
|
||||||
const { notificationService } = t.context;
|
|
||||||
await t.throwsAsync(notificationService.markAsRead(member.id, randomUUID()), {
|
await t.throwsAsync(notificationService.markAsRead(member.id, randomUUID()), {
|
||||||
instanceOf: NotificationNotFound,
|
instanceOf: NotificationNotFound,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error on mark notification as read if notification user is not the same', async t => {
|
test('should throw error on mark notification as read if notification user is not the same', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const notification = await notificationService.createInvitation({
|
const notification = await notificationService.createInvitation({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -431,7 +453,9 @@ test('should throw error on mark notification as read if notification user is no
|
|||||||
inviteId: randomUUID(),
|
inviteId: randomUUID(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const otherUser = await module.create(Mockers.User);
|
const otherUser = await module.create(Mockers.User);
|
||||||
|
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
notificationService.markAsRead(otherUser.id, notification!.id),
|
notificationService.markAsRead(otherUser.id, notification!.id),
|
||||||
{
|
{
|
||||||
@@ -441,8 +465,8 @@ test('should throw error on mark notification as read if notification user is no
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should use latest doc title in mention notification', async t => {
|
test('should use latest doc title in mention notification', async t => {
|
||||||
const { notificationService, models } = t.context;
|
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
|
|
||||||
await notificationService.createMention({
|
await notificationService.createMention({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -456,6 +480,7 @@ test('should use latest doc title in mention notification', async t => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mentionNotification = await notificationService.createMention({
|
const mentionNotification = await notificationService.createMention({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -469,7 +494,9 @@ test('should use latest doc title in mention notification', async t => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(mentionNotification);
|
t.truthy(mentionNotification);
|
||||||
|
|
||||||
mock.method(models.doc, 'findMetas', async () => [
|
mock.method(models.doc, 'findMetas', async () => [
|
||||||
{
|
{
|
||||||
title: 'doc-title-2-updated',
|
title: 'doc-title-2-updated',
|
||||||
@@ -478,7 +505,9 @@ test('should use latest doc title in mention notification', async t => {
|
|||||||
title: 'doc-title-1-updated',
|
title: 'doc-title-1-updated',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const notifications = await notificationService.findManyByUserId(member.id);
|
const notifications = await notificationService.findManyByUserId(member.id);
|
||||||
|
|
||||||
t.is(notifications.length, 2);
|
t.is(notifications.length, 2);
|
||||||
const mention = notifications[0];
|
const mention = notifications[0];
|
||||||
t.is(mention.body.workspace!.id, workspace.id);
|
t.is(mention.body.workspace!.id, workspace.id);
|
||||||
@@ -498,8 +527,8 @@ test('should use latest doc title in mention notification', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should raw doc title in mention notification if no doc found', async t => {
|
test('should raw doc title in mention notification if no doc found', async t => {
|
||||||
const { notificationService, models } = t.context;
|
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
|
|
||||||
await notificationService.createMention({
|
await notificationService.createMention({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -526,7 +555,9 @@ test('should raw doc title in mention notification if no doc found', async t =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
mock.method(models.doc, 'findMetas', async () => [null, null]);
|
mock.method(models.doc, 'findMetas', async () => [null, null]);
|
||||||
|
|
||||||
const notifications = await notificationService.findManyByUserId(member.id);
|
const notifications = await notificationService.findManyByUserId(member.id);
|
||||||
t.is(notifications.length, 2);
|
t.is(notifications.length, 2);
|
||||||
const mention = notifications[0];
|
const mention = notifications[0];
|
||||||
@@ -545,8 +576,8 @@ test('should raw doc title in mention notification if no doc found', async t =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should send mention email by user setting', async t => {
|
test('should send mention email by user setting', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
|
|
||||||
const notification = await notificationService.createMention({
|
const notification = await notificationService.createMention({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -560,17 +591,21 @@ test('should send mention email by user setting', async t => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
|
|
||||||
// should send mention email
|
// should send mention email
|
||||||
const mentionMail = module.mails.last('Mention');
|
const mentionMail = module.queue.last('notification.sendMail');
|
||||||
t.is(mentionMail.to, member.email);
|
t.is(mentionMail.payload.to, member.email);
|
||||||
|
t.is(mentionMail.payload.name, 'Mention');
|
||||||
|
|
||||||
// update user setting to not receive mention email
|
// update user setting to not receive mention email
|
||||||
const mentionMailCount = module.mails.count('Mention');
|
const mentionMailCount = module.queue.count('notification.sendMail');
|
||||||
await module.create(Mockers.UserSettings, {
|
await module.create(Mockers.UserSettings, {
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
receiveMentionEmail: false,
|
receiveMentionEmail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await notificationService.createMention({
|
await notificationService.createMention({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -584,12 +619,12 @@ test('should send mention email by user setting', async t => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// should not send mention email
|
// should not send mention email
|
||||||
t.is(module.mails.count('Mention'), mentionMailCount);
|
t.is(module.queue.count('notification.sendMail'), mentionMailCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send mention email with use client doc title if server doc title is empty', async t => {
|
test('should send mention email with use client doc title if server doc title is empty', async t => {
|
||||||
const { notificationService, module } = t.context;
|
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
await module.create(Mockers.DocMeta, {
|
await module.create(Mockers.DocMeta, {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -597,6 +632,7 @@ test('should send mention email with use client doc title if server doc title is
|
|||||||
// mock empty title
|
// mock empty title
|
||||||
title: '',
|
title: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = await notificationService.createMention({
|
const notification = await notificationService.createMention({
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
body: {
|
body: {
|
||||||
@@ -610,8 +646,115 @@ test('should send mention email with use client doc title if server doc title is
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
const mentionMail = module.mails.last('Mention');
|
|
||||||
t.is(mentionMail.to, member.email);
|
const mentionMail = module.queue.last('notification.sendMail');
|
||||||
t.is(mentionMail.props.doc.title, 'doc-title-1');
|
t.is(mentionMail.payload.to, member.email);
|
||||||
|
t.is(mentionMail.payload.name, 'Mention');
|
||||||
|
// @ts-expect-error - payload is not typed
|
||||||
|
t.is(mentionMail.payload.props.doc.title, 'doc-title-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send comment notification and email', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const commentId = randomUUID();
|
||||||
|
|
||||||
|
const notification = await notificationService.createComment({
|
||||||
|
userId: member.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: owner.id,
|
||||||
|
doc: {
|
||||||
|
id: docId,
|
||||||
|
title: 'doc-title-1',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
commentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(notification);
|
||||||
|
|
||||||
|
const commentMail = module.queue.last('notification.sendMail');
|
||||||
|
t.is(commentMail.payload.to, member.email);
|
||||||
|
t.is(commentMail.payload.name, 'Comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send comment mention notification and email', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const commentId = randomUUID();
|
||||||
|
const replyId = randomUUID();
|
||||||
|
|
||||||
|
const notification = await notificationService.createComment(
|
||||||
|
{
|
||||||
|
userId: member.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: owner.id,
|
||||||
|
doc: {
|
||||||
|
id: docId,
|
||||||
|
title: 'doc-title-1',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
commentId,
|
||||||
|
replyId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
t.truthy(notification);
|
||||||
|
|
||||||
|
const commentMentionMail = module.queue.last('notification.sendMail');
|
||||||
|
t.is(commentMentionMail.payload.to, member.email);
|
||||||
|
t.is(commentMentionMail.payload.name, 'CommentMention');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send comment email by user setting', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
|
||||||
|
const notification = await notificationService.createComment({
|
||||||
|
userId: member.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: owner.id,
|
||||||
|
doc: {
|
||||||
|
id: docId,
|
||||||
|
title: 'doc-title-1',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
commentId: randomUUID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(notification);
|
||||||
|
|
||||||
|
const commentMail = module.queue.last('notification.sendMail');
|
||||||
|
t.is(commentMail.payload.to, member.email);
|
||||||
|
t.is(commentMail.payload.name, 'Comment');
|
||||||
|
|
||||||
|
// update user setting to not receive comment email
|
||||||
|
const commentMailCount = module.queue.count('notification.sendMail');
|
||||||
|
await module.create(Mockers.UserSettings, {
|
||||||
|
userId: member.id,
|
||||||
|
receiveCommentEmail: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await notificationService.createComment({
|
||||||
|
userId: member.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: owner.id,
|
||||||
|
doc: {
|
||||||
|
id: docId,
|
||||||
|
title: 'doc-title-2',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
commentId: randomUUID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// should not send comment email
|
||||||
|
t.is(module.queue.count('notification.sendMail'), commentMailCount);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
|
||||||
import { JobQueue, OnJob } from '../../base';
|
import { JobQueue, OnJob } from '../../base';
|
||||||
import { Models } from '../../models';
|
import { CommentNotificationBody, Models } from '../../models';
|
||||||
import { NotificationService } from './service';
|
import { NotificationService } from './service';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -29,6 +29,11 @@ declare global {
|
|||||||
userId: string;
|
userId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
};
|
};
|
||||||
|
'notification.sendComment': {
|
||||||
|
userId: string;
|
||||||
|
isMention?: boolean;
|
||||||
|
body: CommentNotificationBody;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,4 +151,19 @@ export class NotificationJob {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnJob('notification.sendComment')
|
||||||
|
async sendComment({
|
||||||
|
userId,
|
||||||
|
isMention,
|
||||||
|
body,
|
||||||
|
}: Jobs['notification.sendComment']) {
|
||||||
|
await this.service.createComment(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
isMention
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Prisma } from '@prisma/client';
|
|||||||
|
|
||||||
import { NotificationNotFound, PaginationInput, URLHelper } from '../../base';
|
import { NotificationNotFound, PaginationInput, URLHelper } from '../../base';
|
||||||
import {
|
import {
|
||||||
|
CommentNotification,
|
||||||
|
CommentNotificationCreate,
|
||||||
DEFAULT_WORKSPACE_NAME,
|
DEFAULT_WORKSPACE_NAME,
|
||||||
InvitationNotificationCreate,
|
InvitationNotificationCreate,
|
||||||
InvitationReviewDeclinedNotificationCreate,
|
InvitationReviewDeclinedNotificationCreate,
|
||||||
@@ -36,6 +38,58 @@ export class NotificationService {
|
|||||||
return await this.models.notification.cleanExpiredNotifications();
|
return await this.models.notification.cleanExpiredNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createComment(input: CommentNotificationCreate, isMention?: boolean) {
|
||||||
|
const notification = isMention
|
||||||
|
? await this.models.notification.createCommentMention(input)
|
||||||
|
: await this.models.notification.createComment(input);
|
||||||
|
await this.sendCommentEmail(input, isMention);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendCommentEmail(
|
||||||
|
input: CommentNotificationCreate,
|
||||||
|
isMention?: boolean
|
||||||
|
) {
|
||||||
|
const userSetting = await this.models.userSettings.get(input.userId);
|
||||||
|
if (!userSetting.receiveCommentEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const receiver = await this.models.user.getWorkspaceUser(input.userId);
|
||||||
|
if (!receiver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const doc = await this.models.doc.getMeta(
|
||||||
|
input.body.workspaceId,
|
||||||
|
input.body.doc.id
|
||||||
|
);
|
||||||
|
const title = doc?.title || input.body.doc.title;
|
||||||
|
const url = this.url.link(
|
||||||
|
generateDocPath({
|
||||||
|
workspaceId: input.body.workspaceId,
|
||||||
|
docId: input.body.doc.id,
|
||||||
|
mode: input.body.doc.mode,
|
||||||
|
blockId: input.body.doc.blockId,
|
||||||
|
elementId: input.body.doc.elementId,
|
||||||
|
commentId: input.body.commentId,
|
||||||
|
replyId: input.body.replyId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await this.mailer.trySend({
|
||||||
|
name: isMention ? 'CommentMention' : 'Comment',
|
||||||
|
to: receiver.email,
|
||||||
|
props: {
|
||||||
|
user: {
|
||||||
|
$$userId: input.body.createdByUserId,
|
||||||
|
},
|
||||||
|
doc: {
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.debug(`Comment email sent to user ${receiver.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
async createMention(input: MentionNotificationCreate) {
|
async createMention(input: MentionNotificationCreate) {
|
||||||
const notification = await this.models.notification.createMention(input);
|
const notification = await this.models.notification.createMention(input);
|
||||||
await this.sendMentionEmail(input);
|
await this.sendMentionEmail(input);
|
||||||
@@ -370,8 +424,11 @@ export class NotificationService {
|
|||||||
|
|
||||||
// fill latest doc title
|
// fill latest doc title
|
||||||
const mentions = notifications.filter(
|
const mentions = notifications.filter(
|
||||||
n => n.type === NotificationType.Mention
|
n =>
|
||||||
) as MentionNotification[];
|
n.type === NotificationType.Mention ||
|
||||||
|
n.type === NotificationType.CommentMention ||
|
||||||
|
n.type === NotificationType.Comment
|
||||||
|
) as (MentionNotification | CommentNotification)[];
|
||||||
const mentionDocs = await this.models.doc.findMetas(
|
const mentionDocs = await this.models.doc.findMetas(
|
||||||
mentions.map(m => ({
|
mentions.map(m => ({
|
||||||
workspaceId: m.body.workspaceId,
|
workspaceId: m.body.workspaceId,
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ export class UserSettingsType implements UserSettings {
|
|||||||
|
|
||||||
@Field({ description: 'Receive mention email' })
|
@Field({ description: 'Receive mention email' })
|
||||||
receiveMentionEmail!: boolean;
|
receiveMentionEmail!: boolean;
|
||||||
|
|
||||||
|
@Field({ description: 'Receive comment email' })
|
||||||
|
receiveCommentEmail!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@@ -145,4 +148,7 @@ export class UpdateUserSettingsInput implements UserSettingsInput {
|
|||||||
|
|
||||||
@Field({ description: 'Receive mention email', nullable: true })
|
@Field({ description: 'Receive mention email', nullable: true })
|
||||||
receiveMentionEmail?: boolean;
|
receiveMentionEmail?: boolean;
|
||||||
|
|
||||||
|
@Field({ description: 'Receive comment email', nullable: true })
|
||||||
|
receiveCommentEmail?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,14 @@ type DocPathParams = {
|
|||||||
mode: DocMode;
|
mode: DocMode;
|
||||||
blockId?: string;
|
blockId?: string;
|
||||||
elementId?: string;
|
elementId?: string;
|
||||||
|
commentId?: string;
|
||||||
|
replyId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To generate a doc url path like
|
* To generate a doc url path like
|
||||||
*
|
*
|
||||||
* /workspace/{workspaceId}/{docId}?mode={DocMode}&elementIds={elementId}&blockIds={blockId}
|
* /workspace/{workspaceId}/{docId}?mode={DocMode}&elementIds={elementId}&blockIds={blockId}&commentId={commentId}&replyId={replyId}
|
||||||
*/
|
*/
|
||||||
export function generateDocPath(params: DocPathParams) {
|
export function generateDocPath(params: DocPathParams) {
|
||||||
const search = new URLSearchParams({
|
const search = new URLSearchParams({
|
||||||
@@ -145,5 +147,11 @@ export function generateDocPath(params: DocPathParams) {
|
|||||||
if (params.blockId) {
|
if (params.blockId) {
|
||||||
search.set('blockIds', params.blockId);
|
search.set('blockIds', params.blockId);
|
||||||
}
|
}
|
||||||
|
if (params.commentId) {
|
||||||
|
search.set('commentId', params.commentId);
|
||||||
|
}
|
||||||
|
if (params.replyId) {
|
||||||
|
search.set('replyId', params.replyId);
|
||||||
|
}
|
||||||
return `/workspace/${params.workspaceId}/${params.docId}?${search.toString()}`;
|
return `/workspace/${params.workspaceId}/${params.docId}?${search.toString()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { TEST_DOC, TEST_USER } from '../common';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Content,
|
||||||
|
Doc,
|
||||||
|
type DocProps,
|
||||||
|
P,
|
||||||
|
Template,
|
||||||
|
Title,
|
||||||
|
User,
|
||||||
|
type UserProps,
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
|
export type CommentMentionProps = {
|
||||||
|
user: UserProps;
|
||||||
|
doc: DocProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommentMention(props: CommentMentionProps) {
|
||||||
|
const { user, doc } = props;
|
||||||
|
return (
|
||||||
|
<Template>
|
||||||
|
<Title>You are mentioned in a comment</Title>
|
||||||
|
<Content>
|
||||||
|
<P>
|
||||||
|
<User {...user} /> mentioned you in a comment on <Doc {...doc} />.
|
||||||
|
</P>
|
||||||
|
<Button href={doc.url}>View Comment</Button>
|
||||||
|
</Content>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CommentMention.PreviewProps = {
|
||||||
|
user: TEST_USER,
|
||||||
|
doc: TEST_DOC,
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { TEST_DOC, TEST_USER } from '../common';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Content,
|
||||||
|
Doc,
|
||||||
|
type DocProps,
|
||||||
|
P,
|
||||||
|
Template,
|
||||||
|
Title,
|
||||||
|
User,
|
||||||
|
type UserProps,
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
|
export type CommentProps = {
|
||||||
|
user: UserProps;
|
||||||
|
doc: DocProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Comment(props: CommentProps) {
|
||||||
|
const { user, doc } = props;
|
||||||
|
return (
|
||||||
|
<Template>
|
||||||
|
<Title>You have a new comment</Title>
|
||||||
|
<Content>
|
||||||
|
<P>
|
||||||
|
<User {...user} /> commented on <Doc {...doc} />.
|
||||||
|
</P>
|
||||||
|
<Button href={doc.url}>View Comment</Button>
|
||||||
|
</Content>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comment.PreviewProps = {
|
||||||
|
user: TEST_USER,
|
||||||
|
doc: TEST_DOC,
|
||||||
|
};
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
export * from './comment';
|
||||||
|
export * from './comment-mention';
|
||||||
export * from './mention';
|
export * from './mention';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render as rawRender } from '@react-email/components';
|
import { render as rawRender } from '@react-email/components';
|
||||||
|
|
||||||
import { Mention } from './docs';
|
import { Comment, CommentMention, Mention } from './docs';
|
||||||
import {
|
import {
|
||||||
TeamBecomeAdmin,
|
TeamBecomeAdmin,
|
||||||
TeamBecomeCollaborator,
|
TeamBecomeCollaborator,
|
||||||
@@ -125,6 +125,15 @@ export const Renderers = {
|
|||||||
Mention,
|
Mention,
|
||||||
props => `${props.user.email} mentioned you in ${props.doc.title}`
|
props => `${props.user.email} mentioned you in ${props.doc.title}`
|
||||||
),
|
),
|
||||||
|
Comment: make(
|
||||||
|
Comment,
|
||||||
|
props => `${props.user.email} commented on ${props.doc.title}`
|
||||||
|
),
|
||||||
|
CommentMention: make(
|
||||||
|
CommentMention,
|
||||||
|
props =>
|
||||||
|
`${props.user.email} mentioned you in a comment on ${props.doc.title}`
|
||||||
|
),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Team
|
//#region Team
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Snapshot report for `src/models/__tests__/user-settings.spec.ts`
|
||||||
|
|
||||||
|
The actual snapshot is saved in `user-settings.spec.ts.snap`.
|
||||||
|
|
||||||
|
Generated by [AVA](https://avajs.dev).
|
||||||
|
|
||||||
|
## should get a user settings with default value
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
{
|
||||||
|
receiveCommentEmail: true,
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
## should update a user settings
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
{
|
||||||
|
receiveCommentEmail: true,
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
> Snapshot 2
|
||||||
|
|
||||||
|
{
|
||||||
|
receiveCommentEmail: true,
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
> Snapshot 3
|
||||||
|
|
||||||
|
{
|
||||||
|
receiveCommentEmail: true,
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
receiveMentionEmail: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
## should set receiveCommentEmail to false
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
{
|
||||||
|
receiveCommentEmail: false,
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: true,
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,10 +1,11 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { mock } from 'node:test';
|
import { mock } from 'node:test';
|
||||||
|
|
||||||
import ava, { TestFn } from 'ava';
|
import test from 'ava';
|
||||||
|
|
||||||
import { createTestingModule, type TestingModule } from '../../__tests__/utils';
|
import { createModule } from '../../__tests__/create-module';
|
||||||
import { Config } from '../../base/config';
|
import { Mockers } from '../../__tests__/mocks';
|
||||||
|
import { Due } from '../../base';
|
||||||
import {
|
import {
|
||||||
DocMode,
|
DocMode,
|
||||||
Models,
|
Models,
|
||||||
@@ -13,38 +14,20 @@ import {
|
|||||||
User,
|
User,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
interface Context {
|
|
||||||
config: Config;
|
|
||||||
module: TestingModule;
|
|
||||||
models: Models;
|
|
||||||
}
|
|
||||||
|
|
||||||
const test = ava as TestFn<Context>;
|
|
||||||
|
|
||||||
test.before(async t => {
|
|
||||||
const module = await createTestingModule();
|
|
||||||
|
|
||||||
t.context.models = module.get(Models);
|
|
||||||
t.context.config = module.get(Config);
|
|
||||||
t.context.module = module;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const module = await createModule();
|
||||||
|
const models = module.get(Models);
|
||||||
let user: User;
|
let user: User;
|
||||||
let createdBy: User;
|
let createdBy: User;
|
||||||
let workspace: Workspace;
|
let workspace: Workspace;
|
||||||
let docId: string;
|
let docId: string;
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
test.beforeEach(async () => {
|
||||||
await t.context.module.initTestingDB();
|
user = await module.create(Mockers.User);
|
||||||
user = await t.context.models.user.create({
|
createdBy = await module.create(Mockers.User);
|
||||||
email: 'test@affine.pro',
|
workspace = await module.create(Mockers.Workspace);
|
||||||
});
|
|
||||||
createdBy = await t.context.models.user.create({
|
|
||||||
email: 'createdBy@affine.pro',
|
|
||||||
});
|
|
||||||
workspace = await t.context.models.workspace.create(user.id);
|
|
||||||
docId = randomUUID();
|
docId = randomUUID();
|
||||||
await t.context.models.doc.upsert({
|
await models.doc.upsert({
|
||||||
spaceId: user.id,
|
spaceId: user.id,
|
||||||
docId,
|
docId,
|
||||||
blob: Buffer.from('hello'),
|
blob: Buffer.from('hello'),
|
||||||
@@ -58,12 +41,12 @@ test.afterEach.always(() => {
|
|||||||
mock.timers.reset();
|
mock.timers.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.after(async t => {
|
test.after.always(async () => {
|
||||||
await t.context.module.close();
|
await module.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create a mention notification with default level', async t => {
|
test('should create a mention notification with default level', async t => {
|
||||||
const notification = await t.context.models.notification.createMention({
|
const notification = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -87,7 +70,7 @@ test('should create a mention notification with default level', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create a mention notification with custom level', async t => {
|
test('should create a mention notification with custom level', async t => {
|
||||||
const notification = await t.context.models.notification.createMention({
|
const notification = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -112,7 +95,7 @@ test('should create a mention notification with custom level', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should mark a mention notification as read', async t => {
|
test('should mark a mention notification as read', async t => {
|
||||||
const notification = await t.context.models.notification.createMention({
|
const notification = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -126,16 +109,14 @@ test('should mark a mention notification as read', async t => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.is(notification.read, false);
|
t.is(notification.read, false);
|
||||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
await models.notification.markAsRead(notification.id, user.id);
|
||||||
const updatedNotification = await t.context.models.notification.get(
|
const updatedNotification = await models.notification.get(notification.id);
|
||||||
notification.id
|
|
||||||
);
|
|
||||||
t.is(updatedNotification!.read, true);
|
t.is(updatedNotification!.read, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create an invite notification', async t => {
|
test('should create an invite notification', async t => {
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
const notification = await t.context.models.notification.createInvitation({
|
const notification = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -152,7 +133,7 @@ test('should create an invite notification', async t => {
|
|||||||
|
|
||||||
test('should mark an invite notification as read', async t => {
|
test('should mark an invite notification as read', async t => {
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
const notification = await t.context.models.notification.createInvitation({
|
const notification = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -161,15 +142,13 @@ test('should mark an invite notification as read', async t => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.is(notification.read, false);
|
t.is(notification.read, false);
|
||||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
await models.notification.markAsRead(notification.id, user.id);
|
||||||
const updatedNotification = await t.context.models.notification.get(
|
const updatedNotification = await models.notification.get(notification.id);
|
||||||
notification.id
|
|
||||||
);
|
|
||||||
t.is(updatedNotification!.read, true);
|
t.is(updatedNotification!.read, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should find many notifications by user id, order by createdAt descending', async t => {
|
test('should find many notifications by user id, order by createdAt descending', async t => {
|
||||||
const notification1 = await t.context.models.notification.createMention({
|
const notification1 = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -183,7 +162,7 @@ test('should find many notifications by user id, order by createdAt descending',
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
const notification2 = await t.context.models.notification.createInvitation({
|
const notification2 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -191,16 +170,14 @@ test('should find many notifications by user id, order by createdAt descending',
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notifications = await t.context.models.notification.findManyByUserId(
|
const notifications = await models.notification.findManyByUserId(user.id);
|
||||||
user.id
|
|
||||||
);
|
|
||||||
t.is(notifications.length, 2);
|
t.is(notifications.length, 2);
|
||||||
t.is(notifications[0].id, notification2.id);
|
t.is(notifications[0].id, notification2.id);
|
||||||
t.is(notifications[1].id, notification1.id);
|
t.is(notifications[1].id, notification1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should find many notifications by user id, filter read notifications', async t => {
|
test('should find many notifications by user id, filter read notifications', async t => {
|
||||||
const notification1 = await t.context.models.notification.createMention({
|
const notification1 = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -214,7 +191,7 @@ test('should find many notifications by user id, filter read notifications', asy
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
const notification2 = await t.context.models.notification.createInvitation({
|
const notification2 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -222,16 +199,14 @@ test('should find many notifications by user id, filter read notifications', asy
|
|||||||
inviteId,
|
inviteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await t.context.models.notification.markAsRead(notification2.id, user.id);
|
await models.notification.markAsRead(notification2.id, user.id);
|
||||||
const notifications = await t.context.models.notification.findManyByUserId(
|
const notifications = await models.notification.findManyByUserId(user.id);
|
||||||
user.id
|
|
||||||
);
|
|
||||||
t.is(notifications.length, 1);
|
t.is(notifications.length, 1);
|
||||||
t.is(notifications[0].id, notification1.id);
|
t.is(notifications[0].id, notification1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should clean expired notifications', async t => {
|
test('should clean expired notifications', async t => {
|
||||||
const notification = await t.context.models.notification.createMention({
|
const notification = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -245,30 +220,28 @@ test('should clean expired notifications', async t => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.truthy(notification);
|
t.truthy(notification);
|
||||||
let notifications = await t.context.models.notification.findManyByUserId(
|
let notifications = await models.notification.findManyByUserId(user.id);
|
||||||
user.id
|
|
||||||
);
|
|
||||||
t.is(notifications.length, 1);
|
t.is(notifications.length, 1);
|
||||||
let count = await t.context.models.notification.cleanExpiredNotifications();
|
let count = await models.notification.cleanExpiredNotifications();
|
||||||
t.is(count, 0);
|
t.is(count, 0);
|
||||||
notifications = await t.context.models.notification.findManyByUserId(user.id);
|
notifications = await models.notification.findManyByUserId(user.id);
|
||||||
t.is(notifications.length, 1);
|
t.is(notifications.length, 1);
|
||||||
t.is(notifications[0].id, notification.id);
|
t.is(notifications[0].id, notification.id);
|
||||||
|
|
||||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
await models.notification.markAsRead(notification.id, user.id);
|
||||||
// wait for 1 year
|
// wait for 1 year
|
||||||
mock.timers.enable({
|
mock.timers.enable({
|
||||||
apis: ['Date'],
|
apis: ['Date'],
|
||||||
now: Date.now() + 1000 * 60 * 60 * 24 * 365,
|
now: Due.after('1y'),
|
||||||
});
|
});
|
||||||
count = await t.context.models.notification.cleanExpiredNotifications();
|
count = await models.notification.cleanExpiredNotifications();
|
||||||
t.is(count, 1);
|
t.true(count > 0);
|
||||||
notifications = await t.context.models.notification.findManyByUserId(user.id);
|
notifications = await models.notification.findManyByUserId(user.id);
|
||||||
t.is(notifications.length, 0);
|
t.is(notifications.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not clean unexpired notifications', async t => {
|
test('should not clean unexpired notifications', async t => {
|
||||||
const notification = await t.context.models.notification.createMention({
|
const notification = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -281,15 +254,15 @@ test('should not clean unexpired notifications', async t => {
|
|||||||
createdByUserId: createdBy.id,
|
createdByUserId: createdBy.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let count = await t.context.models.notification.cleanExpiredNotifications();
|
let count = await models.notification.cleanExpiredNotifications();
|
||||||
t.is(count, 0);
|
t.is(count, 0);
|
||||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
await models.notification.markAsRead(notification.id, user.id);
|
||||||
count = await t.context.models.notification.cleanExpiredNotifications();
|
count = await models.notification.cleanExpiredNotifications();
|
||||||
t.is(count, 0);
|
t.is(count, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should find many notifications by user id, order by createdAt descending, with pagination', async t => {
|
test('should find many notifications by user id, order by createdAt descending, with pagination', async t => {
|
||||||
const notification1 = await t.context.models.notification.createMention({
|
const notification1 = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -302,7 +275,7 @@ test('should find many notifications by user id, order by createdAt descending,
|
|||||||
createdByUserId: createdBy.id,
|
createdByUserId: createdBy.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notification2 = await t.context.models.notification.createInvitation({
|
const notification2 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -310,7 +283,7 @@ test('should find many notifications by user id, order by createdAt descending,
|
|||||||
inviteId: randomUUID(),
|
inviteId: randomUUID(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notification3 = await t.context.models.notification.createInvitation({
|
const notification3 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -318,7 +291,7 @@ test('should find many notifications by user id, order by createdAt descending,
|
|||||||
inviteId: randomUUID(),
|
inviteId: randomUUID(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notification4 = await t.context.models.notification.createInvitation({
|
const notification4 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -326,38 +299,29 @@ test('should find many notifications by user id, order by createdAt descending,
|
|||||||
inviteId: randomUUID(),
|
inviteId: randomUUID(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notifications = await t.context.models.notification.findManyByUserId(
|
const notifications = await models.notification.findManyByUserId(user.id, {
|
||||||
user.id,
|
offset: 0,
|
||||||
{
|
first: 2,
|
||||||
offset: 0,
|
});
|
||||||
first: 2,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
t.is(notifications.length, 2);
|
t.is(notifications.length, 2);
|
||||||
t.is(notifications[0].id, notification4.id);
|
t.is(notifications[0].id, notification4.id);
|
||||||
t.is(notifications[1].id, notification3.id);
|
t.is(notifications[1].id, notification3.id);
|
||||||
const notifications2 = await t.context.models.notification.findManyByUserId(
|
const notifications2 = await models.notification.findManyByUserId(user.id, {
|
||||||
user.id,
|
offset: 2,
|
||||||
{
|
first: 2,
|
||||||
offset: 2,
|
});
|
||||||
first: 2,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
t.is(notifications2.length, 2);
|
t.is(notifications2.length, 2);
|
||||||
t.is(notifications2[0].id, notification2.id);
|
t.is(notifications2[0].id, notification2.id);
|
||||||
t.is(notifications2[1].id, notification1.id);
|
t.is(notifications2[1].id, notification1.id);
|
||||||
const notifications3 = await t.context.models.notification.findManyByUserId(
|
const notifications3 = await models.notification.findManyByUserId(user.id, {
|
||||||
user.id,
|
offset: 4,
|
||||||
{
|
first: 2,
|
||||||
offset: 4,
|
});
|
||||||
first: 2,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
t.is(notifications3.length, 0);
|
t.is(notifications3.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should count notifications by user id, exclude read notifications', async t => {
|
test('should count notifications by user id, exclude read notifications', async t => {
|
||||||
const notification1 = await t.context.models.notification.createMention({
|
const notification1 = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -371,7 +335,7 @@ test('should count notifications by user id, exclude read notifications', async
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.truthy(notification1);
|
t.truthy(notification1);
|
||||||
const notification2 = await t.context.models.notification.createInvitation({
|
const notification2 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -380,13 +344,13 @@ test('should count notifications by user id, exclude read notifications', async
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.truthy(notification2);
|
t.truthy(notification2);
|
||||||
await t.context.models.notification.markAsRead(notification2.id, user.id);
|
await models.notification.markAsRead(notification2.id, user.id);
|
||||||
const count = await t.context.models.notification.countByUserId(user.id);
|
const count = await models.notification.countByUserId(user.id);
|
||||||
t.is(count, 1);
|
t.is(count, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should count notifications by user id, include read notifications', async t => {
|
test('should count notifications by user id, include read notifications', async t => {
|
||||||
const notification1 = await t.context.models.notification.createMention({
|
const notification1 = await models.notification.createMention({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -400,7 +364,7 @@ test('should count notifications by user id, include read notifications', async
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.truthy(notification1);
|
t.truthy(notification1);
|
||||||
const notification2 = await t.context.models.notification.createInvitation({
|
const notification2 = await models.notification.createInvitation({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@@ -409,9 +373,60 @@ test('should count notifications by user id, include read notifications', async
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
t.truthy(notification2);
|
t.truthy(notification2);
|
||||||
await t.context.models.notification.markAsRead(notification2.id, user.id);
|
await models.notification.markAsRead(notification2.id, user.id);
|
||||||
const count = await t.context.models.notification.countByUserId(user.id, {
|
const count = await models.notification.countByUserId(user.id, {
|
||||||
includeRead: true,
|
includeRead: true,
|
||||||
});
|
});
|
||||||
t.is(count, 2);
|
t.is(count, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create a comment notification', async t => {
|
||||||
|
const commentId = randomUUID();
|
||||||
|
|
||||||
|
const notification = await models.notification.createComment({
|
||||||
|
userId: user.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
doc: {
|
||||||
|
id: docId,
|
||||||
|
title: 'doc-title',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
createdByUserId: createdBy.id,
|
||||||
|
commentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(notification.type, NotificationType.Comment);
|
||||||
|
t.is(notification.body.workspaceId, workspace.id);
|
||||||
|
t.is(notification.body.doc.id, docId);
|
||||||
|
t.is(notification.body.doc.title, 'doc-title');
|
||||||
|
t.is(notification.body.commentId, commentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create a comment mention notification', async t => {
|
||||||
|
const commentId = randomUUID();
|
||||||
|
const replyId = randomUUID();
|
||||||
|
|
||||||
|
const notification = await models.notification.createCommentMention({
|
||||||
|
userId: user.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
doc: {
|
||||||
|
id: docId,
|
||||||
|
title: 'doc-title',
|
||||||
|
mode: DocMode.page,
|
||||||
|
},
|
||||||
|
createdByUserId: createdBy.id,
|
||||||
|
commentId,
|
||||||
|
replyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(notification.type, NotificationType.CommentMention);
|
||||||
|
t.is(notification.body.workspaceId, workspace.id);
|
||||||
|
t.is(notification.body.doc.id, docId);
|
||||||
|
t.is(notification.body.doc.title, 'doc-title');
|
||||||
|
t.is(notification.body.commentId, commentId);
|
||||||
|
t.is(notification.body.replyId, replyId);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,92 +1,80 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import test from 'ava';
|
||||||
import { mock } from 'node:test';
|
|
||||||
|
|
||||||
import ava, { TestFn } from 'ava';
|
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
import { createTestingModule, type TestingModule } from '../../__tests__/utils';
|
import { createModule } from '../../__tests__/create-module';
|
||||||
import { Config } from '../../base/config';
|
import { Mockers } from '../../__tests__/mocks';
|
||||||
import { Models, User } from '..';
|
import { Models } from '..';
|
||||||
|
|
||||||
interface Context {
|
const module = await createModule();
|
||||||
config: Config;
|
const models = module.get(Models);
|
||||||
module: TestingModule;
|
|
||||||
models: Models;
|
|
||||||
}
|
|
||||||
|
|
||||||
const test = ava as TestFn<Context>;
|
test.after.always(async () => {
|
||||||
|
await module.close();
|
||||||
test.before(async t => {
|
|
||||||
const module = await createTestingModule();
|
|
||||||
|
|
||||||
t.context.models = module.get(Models);
|
|
||||||
t.context.config = module.get(Config);
|
|
||||||
t.context.module = module;
|
|
||||||
await t.context.module.initTestingDB();
|
|
||||||
});
|
|
||||||
|
|
||||||
let user: User;
|
|
||||||
|
|
||||||
test.beforeEach(async t => {
|
|
||||||
user = await t.context.models.user.create({
|
|
||||||
email: `test-${randomUUID()}@affine.pro`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterEach.always(() => {
|
|
||||||
mock.reset();
|
|
||||||
mock.timers.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.after(async t => {
|
|
||||||
await t.context.module.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get a user settings with default value', async t => {
|
test('should get a user settings with default value', async t => {
|
||||||
const settings = await t.context.models.userSettings.get(user.id);
|
const user = await module.create(Mockers.User);
|
||||||
t.deepEqual(settings, {
|
|
||||||
receiveInvitationEmail: true,
|
const settings = await models.userSettings.get(user.id);
|
||||||
receiveMentionEmail: true,
|
|
||||||
});
|
t.snapshot(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update a user settings', async t => {
|
test('should update a user settings', async t => {
|
||||||
const settings = await t.context.models.userSettings.set(user.id, {
|
const user = await module.create(Mockers.User);
|
||||||
|
|
||||||
|
const settings = await models.userSettings.set(user.id, {
|
||||||
receiveInvitationEmail: false,
|
receiveInvitationEmail: false,
|
||||||
});
|
});
|
||||||
t.deepEqual(settings, {
|
|
||||||
receiveInvitationEmail: false,
|
t.snapshot(settings);
|
||||||
receiveMentionEmail: true,
|
|
||||||
});
|
const settings2 = await models.userSettings.get(user.id);
|
||||||
const settings2 = await t.context.models.userSettings.get(user.id);
|
|
||||||
t.deepEqual(settings2, settings);
|
t.deepEqual(settings2, settings);
|
||||||
|
|
||||||
// update existing setting
|
// update existing setting
|
||||||
const setting3 = await t.context.models.userSettings.set(user.id, {
|
const setting3 = await models.userSettings.set(user.id, {
|
||||||
receiveInvitationEmail: true,
|
receiveInvitationEmail: true,
|
||||||
});
|
});
|
||||||
t.deepEqual(setting3, {
|
|
||||||
receiveInvitationEmail: true,
|
t.snapshot(setting3);
|
||||||
receiveMentionEmail: true,
|
|
||||||
});
|
const setting4 = await models.userSettings.get(user.id);
|
||||||
const setting4 = await t.context.models.userSettings.get(user.id);
|
|
||||||
t.deepEqual(setting4, setting3);
|
t.deepEqual(setting4, setting3);
|
||||||
|
|
||||||
const setting5 = await t.context.models.userSettings.set(user.id, {
|
const setting5 = await models.userSettings.set(user.id, {
|
||||||
receiveMentionEmail: false,
|
receiveMentionEmail: false,
|
||||||
receiveInvitationEmail: false,
|
receiveInvitationEmail: false,
|
||||||
});
|
});
|
||||||
t.deepEqual(setting5, {
|
|
||||||
receiveInvitationEmail: false,
|
t.snapshot(setting5);
|
||||||
receiveMentionEmail: false,
|
|
||||||
});
|
const setting6 = await models.userSettings.get(user.id);
|
||||||
const setting6 = await t.context.models.userSettings.get(user.id);
|
|
||||||
t.deepEqual(setting6, setting5);
|
t.deepEqual(setting6, setting5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should set receiveCommentEmail to false', async t => {
|
||||||
|
const user = await module.create(Mockers.User);
|
||||||
|
|
||||||
|
const settings = await models.userSettings.set(user.id, {
|
||||||
|
receiveCommentEmail: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.snapshot(settings);
|
||||||
|
|
||||||
|
const settings2 = await models.userSettings.get(user.id);
|
||||||
|
|
||||||
|
t.deepEqual(settings2, settings);
|
||||||
|
});
|
||||||
|
|
||||||
test('should throw error when update settings with invalid payload', async t => {
|
test('should throw error when update settings with invalid payload', async t => {
|
||||||
|
const user = await module.create(Mockers.User);
|
||||||
|
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
t.context.models.userSettings.set(user.id, {
|
models.userSettings.set(user.id, {
|
||||||
// @ts-expect-error invalid setting input types
|
// @ts-expect-error invalid setting input types
|
||||||
receiveInvitationEmail: 1,
|
receiveInvitationEmail: 1,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { PaginationInput } from '../base';
|
import { Due, PaginationInput } from '../base';
|
||||||
import { BaseModel } from './base';
|
import { BaseModel } from './base';
|
||||||
import { DocMode } from './common';
|
import { DocMode } from './common';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export type { Notification };
|
|||||||
|
|
||||||
// #region input
|
// #region input
|
||||||
|
|
||||||
export const ONE_YEAR = 1000 * 60 * 60 * 24 * 365;
|
export const ONE_YEAR = Due.ms('1y');
|
||||||
const IdSchema = z.string().trim().min(1).max(100);
|
const IdSchema = z.string().trim().min(1).max(100);
|
||||||
|
|
||||||
export const BaseNotificationCreateSchema = z.object({
|
export const BaseNotificationCreateSchema = z.object({
|
||||||
@@ -96,10 +96,37 @@ export type InvitationReviewDeclinedNotificationCreate = z.input<
|
|||||||
typeof InvitationReviewDeclinedNotificationCreateSchema
|
typeof InvitationReviewDeclinedNotificationCreateSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const CommentNotificationBodySchema = z.object({
|
||||||
|
workspaceId: IdSchema,
|
||||||
|
createdByUserId: IdSchema,
|
||||||
|
commentId: IdSchema,
|
||||||
|
replyId: IdSchema.optional(),
|
||||||
|
doc: MentionDocSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CommentNotificationBody = z.infer<
|
||||||
|
typeof CommentNotificationBodySchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CommentNotificationCreateSchema =
|
||||||
|
BaseNotificationCreateSchema.extend({
|
||||||
|
body: CommentNotificationBodySchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CommentNotificationCreate = z.input<
|
||||||
|
typeof CommentNotificationCreateSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CommentMentionNotificationCreateSchema =
|
||||||
|
BaseNotificationCreateSchema.extend({
|
||||||
|
body: CommentNotificationBodySchema,
|
||||||
|
});
|
||||||
|
|
||||||
export type UnionNotificationBody =
|
export type UnionNotificationBody =
|
||||||
| MentionNotificationBody
|
| MentionNotificationBody
|
||||||
| InvitationNotificationBody
|
| InvitationNotificationBody
|
||||||
| InvitationReviewDeclinedNotificationBody;
|
| InvitationReviewDeclinedNotificationBody
|
||||||
|
| CommentNotificationBody;
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
@@ -114,10 +141,14 @@ export type InvitationNotification = Notification &
|
|||||||
export type InvitationReviewDeclinedNotification = Notification &
|
export type InvitationReviewDeclinedNotification = Notification &
|
||||||
z.infer<typeof InvitationReviewDeclinedNotificationCreateSchema>;
|
z.infer<typeof InvitationReviewDeclinedNotificationCreateSchema>;
|
||||||
|
|
||||||
|
export type CommentNotification = Notification &
|
||||||
|
z.infer<typeof CommentNotificationCreateSchema>;
|
||||||
|
|
||||||
export type UnionNotification =
|
export type UnionNotification =
|
||||||
| MentionNotification
|
| MentionNotification
|
||||||
| InvitationNotification
|
| InvitationNotification
|
||||||
| InvitationReviewDeclinedNotification;
|
| InvitationReviewDeclinedNotification
|
||||||
|
| CommentNotification;
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
@@ -179,6 +210,40 @@ export class NotificationModel extends BaseModel {
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region comment
|
||||||
|
|
||||||
|
async createComment(input: CommentNotificationCreate) {
|
||||||
|
const data = CommentNotificationCreateSchema.parse(input);
|
||||||
|
const type = NotificationType.Comment;
|
||||||
|
const row = await this.create({
|
||||||
|
userId: data.userId,
|
||||||
|
level: data.level,
|
||||||
|
type,
|
||||||
|
body: data.body,
|
||||||
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
`Created ${type} notification ${row.id} to user ${data.userId} in workspace ${data.body.workspaceId}`
|
||||||
|
);
|
||||||
|
return row as CommentNotification;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCommentMention(input: CommentNotificationCreate) {
|
||||||
|
const data = CommentMentionNotificationCreateSchema.parse(input);
|
||||||
|
const type = NotificationType.CommentMention;
|
||||||
|
const row = await this.create({
|
||||||
|
userId: data.userId,
|
||||||
|
level: data.level,
|
||||||
|
type,
|
||||||
|
body: data.body,
|
||||||
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
`Created ${type} notification ${row.id} to user ${data.userId} in workspace ${data.body.workspaceId}`
|
||||||
|
);
|
||||||
|
return row as CommentNotification;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
// #region common
|
// #region common
|
||||||
|
|
||||||
private async create(data: Prisma.NotificationUncheckedCreateInput) {
|
private async create(data: Prisma.NotificationUncheckedCreateInput) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BaseModel } from './base';
|
|||||||
export const UserSettingsSchema = z.object({
|
export const UserSettingsSchema = z.object({
|
||||||
receiveInvitationEmail: z.boolean().default(true),
|
receiveInvitationEmail: z.boolean().default(true),
|
||||||
receiveMentionEmail: z.boolean().default(true),
|
receiveMentionEmail: z.boolean().default(true),
|
||||||
|
receiveCommentEmail: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UserSettingsInput = z.input<typeof UserSettingsSchema>;
|
export type UserSettingsInput = z.input<typeof UserSettingsSchema>;
|
||||||
|
|||||||
@@ -1361,6 +1361,8 @@ type NotificationObjectTypeEdge {
|
|||||||
|
|
||||||
"""Notification type"""
|
"""Notification type"""
|
||||||
enum NotificationType {
|
enum NotificationType {
|
||||||
|
Comment
|
||||||
|
CommentMention
|
||||||
Invitation
|
Invitation
|
||||||
InvitationAccepted
|
InvitationAccepted
|
||||||
InvitationBlocked
|
InvitationBlocked
|
||||||
@@ -1933,6 +1935,9 @@ input UpdateUserInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input UpdateUserSettingsInput {
|
input UpdateUserSettingsInput {
|
||||||
|
"""Receive comment email"""
|
||||||
|
receiveCommentEmail: Boolean
|
||||||
|
|
||||||
"""Receive invitation email"""
|
"""Receive invitation email"""
|
||||||
receiveInvitationEmail: Boolean
|
receiveInvitationEmail: Boolean
|
||||||
|
|
||||||
@@ -1993,6 +1998,9 @@ type UserQuotaUsageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserSettingsType {
|
type UserSettingsType {
|
||||||
|
"""Receive comment email"""
|
||||||
|
receiveCommentEmail: Boolean!
|
||||||
|
|
||||||
"""Receive invitation email"""
|
"""Receive invitation email"""
|
||||||
receiveInvitationEmail: Boolean!
|
receiveInvitationEmail: Boolean!
|
||||||
|
|
||||||
|
|||||||
@@ -1896,6 +1896,8 @@ export interface NotificationObjectTypeEdge {
|
|||||||
|
|
||||||
/** Notification type */
|
/** Notification type */
|
||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
|
Comment = 'Comment',
|
||||||
|
CommentMention = 'CommentMention',
|
||||||
Invitation = 'Invitation',
|
Invitation = 'Invitation',
|
||||||
InvitationAccepted = 'InvitationAccepted',
|
InvitationAccepted = 'InvitationAccepted',
|
||||||
InvitationBlocked = 'InvitationBlocked',
|
InvitationBlocked = 'InvitationBlocked',
|
||||||
@@ -2530,6 +2532,8 @@ export interface UpdateUserInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateUserSettingsInput {
|
export interface UpdateUserSettingsInput {
|
||||||
|
/** Receive comment email */
|
||||||
|
receiveCommentEmail?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
/** Receive invitation email */
|
/** Receive invitation email */
|
||||||
receiveInvitationEmail?: InputMaybe<Scalars['Boolean']['input']>;
|
receiveInvitationEmail?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
/** Receive mention email */
|
/** Receive mention email */
|
||||||
@@ -2589,6 +2593,8 @@ export interface UserQuotaUsageType {
|
|||||||
|
|
||||||
export interface UserSettingsType {
|
export interface UserSettingsType {
|
||||||
__typename?: 'UserSettingsType';
|
__typename?: 'UserSettingsType';
|
||||||
|
/** Receive comment email */
|
||||||
|
receiveCommentEmail: Scalars['Boolean']['output'];
|
||||||
/** Receive invitation email */
|
/** Receive invitation email */
|
||||||
receiveInvitationEmail: Scalars['Boolean']['output'];
|
receiveInvitationEmail: Scalars['Boolean']['output'];
|
||||||
/** Receive mention email */
|
/** Receive mention email */
|
||||||
|
|||||||
Reference in New Issue
Block a user