mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 00:41:39 +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:
@@ -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 { mock } from 'node:test';
|
||||
|
||||
import ava, { TestFn } from 'ava';
|
||||
import test from 'ava';
|
||||
|
||||
import { createTestingModule, type TestingModule } from '../../__tests__/utils';
|
||||
import { Config } from '../../base/config';
|
||||
import { createModule } from '../../__tests__/create-module';
|
||||
import { Mockers } from '../../__tests__/mocks';
|
||||
import { Due } from '../../base';
|
||||
import {
|
||||
DocMode,
|
||||
Models,
|
||||
@@ -13,38 +14,20 @@ import {
|
||||
User,
|
||||
Workspace,
|
||||
} 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 createdBy: User;
|
||||
let workspace: Workspace;
|
||||
let docId: string;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await t.context.module.initTestingDB();
|
||||
user = await t.context.models.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
createdBy = await t.context.models.user.create({
|
||||
email: 'createdBy@affine.pro',
|
||||
});
|
||||
workspace = await t.context.models.workspace.create(user.id);
|
||||
test.beforeEach(async () => {
|
||||
user = await module.create(Mockers.User);
|
||||
createdBy = await module.create(Mockers.User);
|
||||
workspace = await module.create(Mockers.Workspace);
|
||||
docId = randomUUID();
|
||||
await t.context.models.doc.upsert({
|
||||
await models.doc.upsert({
|
||||
spaceId: user.id,
|
||||
docId,
|
||||
blob: Buffer.from('hello'),
|
||||
@@ -58,12 +41,12 @@ test.afterEach.always(() => {
|
||||
mock.timers.reset();
|
||||
});
|
||||
|
||||
test.after(async t => {
|
||||
await t.context.module.close();
|
||||
test.after.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
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 => {
|
||||
const notification = await t.context.models.notification.createMention({
|
||||
const notification = await models.notification.createMention({
|
||||
userId: user.id,
|
||||
body: {
|
||||
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 => {
|
||||
const notification = await t.context.models.notification.createMention({
|
||||
const notification = await models.notification.createMention({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -126,16 +109,14 @@ test('should mark a mention notification as read', async t => {
|
||||
},
|
||||
});
|
||||
t.is(notification.read, false);
|
||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
||||
const updatedNotification = await t.context.models.notification.get(
|
||||
notification.id
|
||||
);
|
||||
await models.notification.markAsRead(notification.id, user.id);
|
||||
const updatedNotification = await models.notification.get(notification.id);
|
||||
t.is(updatedNotification!.read, true);
|
||||
});
|
||||
|
||||
test('should create an invite notification', async t => {
|
||||
const inviteId = randomUUID();
|
||||
const notification = await t.context.models.notification.createInvitation({
|
||||
const notification = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
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 => {
|
||||
const inviteId = randomUUID();
|
||||
const notification = await t.context.models.notification.createInvitation({
|
||||
const notification = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -161,15 +142,13 @@ test('should mark an invite notification as read', async t => {
|
||||
},
|
||||
});
|
||||
t.is(notification.read, false);
|
||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
||||
const updatedNotification = await t.context.models.notification.get(
|
||||
notification.id
|
||||
);
|
||||
await models.notification.markAsRead(notification.id, user.id);
|
||||
const updatedNotification = await models.notification.get(notification.id);
|
||||
t.is(updatedNotification!.read, true);
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -183,7 +162,7 @@ test('should find many notifications by user id, order by createdAt descending',
|
||||
},
|
||||
});
|
||||
const inviteId = randomUUID();
|
||||
const notification2 = await t.context.models.notification.createInvitation({
|
||||
const notification2 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -191,16 +170,14 @@ test('should find many notifications by user id, order by createdAt descending',
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
const notifications = await t.context.models.notification.findManyByUserId(
|
||||
user.id
|
||||
);
|
||||
const notifications = await models.notification.findManyByUserId(user.id);
|
||||
t.is(notifications.length, 2);
|
||||
t.is(notifications[0].id, notification2.id);
|
||||
t.is(notifications[1].id, notification1.id);
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -214,7 +191,7 @@ test('should find many notifications by user id, filter read notifications', asy
|
||||
},
|
||||
});
|
||||
const inviteId = randomUUID();
|
||||
const notification2 = await t.context.models.notification.createInvitation({
|
||||
const notification2 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -222,16 +199,14 @@ test('should find many notifications by user id, filter read notifications', asy
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
await t.context.models.notification.markAsRead(notification2.id, user.id);
|
||||
const notifications = await t.context.models.notification.findManyByUserId(
|
||||
user.id
|
||||
);
|
||||
await models.notification.markAsRead(notification2.id, user.id);
|
||||
const notifications = await models.notification.findManyByUserId(user.id);
|
||||
t.is(notifications.length, 1);
|
||||
t.is(notifications[0].id, notification1.id);
|
||||
});
|
||||
|
||||
test('should clean expired notifications', async t => {
|
||||
const notification = await t.context.models.notification.createMention({
|
||||
const notification = await models.notification.createMention({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -245,30 +220,28 @@ test('should clean expired notifications', async t => {
|
||||
},
|
||||
});
|
||||
t.truthy(notification);
|
||||
let notifications = await t.context.models.notification.findManyByUserId(
|
||||
user.id
|
||||
);
|
||||
let notifications = await models.notification.findManyByUserId(user.id);
|
||||
t.is(notifications.length, 1);
|
||||
let count = await t.context.models.notification.cleanExpiredNotifications();
|
||||
let count = await models.notification.cleanExpiredNotifications();
|
||||
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[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
|
||||
mock.timers.enable({
|
||||
apis: ['Date'],
|
||||
now: Date.now() + 1000 * 60 * 60 * 24 * 365,
|
||||
now: Due.after('1y'),
|
||||
});
|
||||
count = await t.context.models.notification.cleanExpiredNotifications();
|
||||
t.is(count, 1);
|
||||
notifications = await t.context.models.notification.findManyByUserId(user.id);
|
||||
count = await models.notification.cleanExpiredNotifications();
|
||||
t.true(count > 0);
|
||||
notifications = await models.notification.findManyByUserId(user.id);
|
||||
t.is(notifications.length, 0);
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -281,15 +254,15 @@ test('should not clean unexpired notifications', async t => {
|
||||
createdByUserId: createdBy.id,
|
||||
},
|
||||
});
|
||||
let count = await t.context.models.notification.cleanExpiredNotifications();
|
||||
let count = await models.notification.cleanExpiredNotifications();
|
||||
t.is(count, 0);
|
||||
await t.context.models.notification.markAsRead(notification.id, user.id);
|
||||
count = await t.context.models.notification.cleanExpiredNotifications();
|
||||
await models.notification.markAsRead(notification.id, user.id);
|
||||
count = await models.notification.cleanExpiredNotifications();
|
||||
t.is(count, 0);
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -302,7 +275,7 @@ test('should find many notifications by user id, order by createdAt descending,
|
||||
createdByUserId: createdBy.id,
|
||||
},
|
||||
});
|
||||
const notification2 = await t.context.models.notification.createInvitation({
|
||||
const notification2 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -310,7 +283,7 @@ test('should find many notifications by user id, order by createdAt descending,
|
||||
inviteId: randomUUID(),
|
||||
},
|
||||
});
|
||||
const notification3 = await t.context.models.notification.createInvitation({
|
||||
const notification3 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -318,7 +291,7 @@ test('should find many notifications by user id, order by createdAt descending,
|
||||
inviteId: randomUUID(),
|
||||
},
|
||||
});
|
||||
const notification4 = await t.context.models.notification.createInvitation({
|
||||
const notification4 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -326,38 +299,29 @@ test('should find many notifications by user id, order by createdAt descending,
|
||||
inviteId: randomUUID(),
|
||||
},
|
||||
});
|
||||
const notifications = await t.context.models.notification.findManyByUserId(
|
||||
user.id,
|
||||
{
|
||||
offset: 0,
|
||||
first: 2,
|
||||
}
|
||||
);
|
||||
const notifications = await models.notification.findManyByUserId(user.id, {
|
||||
offset: 0,
|
||||
first: 2,
|
||||
});
|
||||
t.is(notifications.length, 2);
|
||||
t.is(notifications[0].id, notification4.id);
|
||||
t.is(notifications[1].id, notification3.id);
|
||||
const notifications2 = await t.context.models.notification.findManyByUserId(
|
||||
user.id,
|
||||
{
|
||||
offset: 2,
|
||||
first: 2,
|
||||
}
|
||||
);
|
||||
const notifications2 = await models.notification.findManyByUserId(user.id, {
|
||||
offset: 2,
|
||||
first: 2,
|
||||
});
|
||||
t.is(notifications2.length, 2);
|
||||
t.is(notifications2[0].id, notification2.id);
|
||||
t.is(notifications2[1].id, notification1.id);
|
||||
const notifications3 = await t.context.models.notification.findManyByUserId(
|
||||
user.id,
|
||||
{
|
||||
offset: 4,
|
||||
first: 2,
|
||||
}
|
||||
);
|
||||
const notifications3 = await models.notification.findManyByUserId(user.id, {
|
||||
offset: 4,
|
||||
first: 2,
|
||||
});
|
||||
t.is(notifications3.length, 0);
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -371,7 +335,7 @@ test('should count notifications by user id, exclude read notifications', async
|
||||
},
|
||||
});
|
||||
t.truthy(notification1);
|
||||
const notification2 = await t.context.models.notification.createInvitation({
|
||||
const notification2 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -380,13 +344,13 @@ test('should count notifications by user id, exclude read notifications', async
|
||||
},
|
||||
});
|
||||
t.truthy(notification2);
|
||||
await t.context.models.notification.markAsRead(notification2.id, user.id);
|
||||
const count = await t.context.models.notification.countByUserId(user.id);
|
||||
await models.notification.markAsRead(notification2.id, user.id);
|
||||
const count = await models.notification.countByUserId(user.id);
|
||||
t.is(count, 1);
|
||||
});
|
||||
|
||||
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,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -400,7 +364,7 @@ test('should count notifications by user id, include read notifications', async
|
||||
},
|
||||
});
|
||||
t.truthy(notification1);
|
||||
const notification2 = await t.context.models.notification.createInvitation({
|
||||
const notification2 = await models.notification.createInvitation({
|
||||
userId: user.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
@@ -409,9 +373,60 @@ test('should count notifications by user id, include read notifications', async
|
||||
},
|
||||
});
|
||||
t.truthy(notification2);
|
||||
await t.context.models.notification.markAsRead(notification2.id, user.id);
|
||||
const count = await t.context.models.notification.countByUserId(user.id, {
|
||||
await models.notification.markAsRead(notification2.id, user.id);
|
||||
const count = await models.notification.countByUserId(user.id, {
|
||||
includeRead: true,
|
||||
});
|
||||
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 { mock } from 'node:test';
|
||||
|
||||
import ava, { TestFn } from 'ava';
|
||||
import test from 'ava';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { createTestingModule, type TestingModule } from '../../__tests__/utils';
|
||||
import { Config } from '../../base/config';
|
||||
import { Models, User } from '..';
|
||||
import { createModule } from '../../__tests__/create-module';
|
||||
import { Mockers } from '../../__tests__/mocks';
|
||||
import { Models } from '..';
|
||||
|
||||
interface Context {
|
||||
config: Config;
|
||||
module: TestingModule;
|
||||
models: Models;
|
||||
}
|
||||
const module = await createModule();
|
||||
const models = module.get(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;
|
||||
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.after.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should get a user settings with default value', async t => {
|
||||
const settings = await t.context.models.userSettings.get(user.id);
|
||||
t.deepEqual(settings, {
|
||||
receiveInvitationEmail: true,
|
||||
receiveMentionEmail: true,
|
||||
});
|
||||
const user = await module.create(Mockers.User);
|
||||
|
||||
const settings = await models.userSettings.get(user.id);
|
||||
|
||||
t.snapshot(settings);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
t.deepEqual(settings, {
|
||||
receiveInvitationEmail: false,
|
||||
receiveMentionEmail: true,
|
||||
});
|
||||
const settings2 = await t.context.models.userSettings.get(user.id);
|
||||
|
||||
t.snapshot(settings);
|
||||
|
||||
const settings2 = await models.userSettings.get(user.id);
|
||||
|
||||
t.deepEqual(settings2, settings);
|
||||
|
||||
// update existing setting
|
||||
const setting3 = await t.context.models.userSettings.set(user.id, {
|
||||
const setting3 = await models.userSettings.set(user.id, {
|
||||
receiveInvitationEmail: true,
|
||||
});
|
||||
t.deepEqual(setting3, {
|
||||
receiveInvitationEmail: true,
|
||||
receiveMentionEmail: true,
|
||||
});
|
||||
const setting4 = await t.context.models.userSettings.get(user.id);
|
||||
|
||||
t.snapshot(setting3);
|
||||
|
||||
const setting4 = await models.userSettings.get(user.id);
|
||||
|
||||
t.deepEqual(setting4, setting3);
|
||||
|
||||
const setting5 = await t.context.models.userSettings.set(user.id, {
|
||||
const setting5 = await models.userSettings.set(user.id, {
|
||||
receiveMentionEmail: false,
|
||||
receiveInvitationEmail: false,
|
||||
});
|
||||
t.deepEqual(setting5, {
|
||||
receiveInvitationEmail: false,
|
||||
receiveMentionEmail: false,
|
||||
});
|
||||
const setting6 = await t.context.models.userSettings.get(user.id);
|
||||
|
||||
t.snapshot(setting5);
|
||||
|
||||
const setting6 = await models.userSettings.get(user.id);
|
||||
|
||||
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 => {
|
||||
const user = await module.create(Mockers.User);
|
||||
|
||||
await t.throwsAsync(
|
||||
t.context.models.userSettings.set(user.id, {
|
||||
models.userSettings.set(user.id, {
|
||||
// @ts-expect-error invalid setting input types
|
||||
receiveInvitationEmail: 1,
|
||||
}),
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PaginationInput } from '../base';
|
||||
import { Due, PaginationInput } from '../base';
|
||||
import { BaseModel } from './base';
|
||||
import { DocMode } from './common';
|
||||
|
||||
@@ -16,7 +16,7 @@ export type { Notification };
|
||||
|
||||
// #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);
|
||||
|
||||
export const BaseNotificationCreateSchema = z.object({
|
||||
@@ -96,10 +96,37 @@ export type InvitationReviewDeclinedNotificationCreate = z.input<
|
||||
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 =
|
||||
| MentionNotificationBody
|
||||
| InvitationNotificationBody
|
||||
| InvitationReviewDeclinedNotificationBody;
|
||||
| InvitationReviewDeclinedNotificationBody
|
||||
| CommentNotificationBody;
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -114,10 +141,14 @@ export type InvitationNotification = Notification &
|
||||
export type InvitationReviewDeclinedNotification = Notification &
|
||||
z.infer<typeof InvitationReviewDeclinedNotificationCreateSchema>;
|
||||
|
||||
export type CommentNotification = Notification &
|
||||
z.infer<typeof CommentNotificationCreateSchema>;
|
||||
|
||||
export type UnionNotification =
|
||||
| MentionNotification
|
||||
| InvitationNotification
|
||||
| InvitationReviewDeclinedNotification;
|
||||
| InvitationReviewDeclinedNotification
|
||||
| CommentNotification;
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -179,6 +210,40 @@ export class NotificationModel extends BaseModel {
|
||||
|
||||
// #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
|
||||
|
||||
private async create(data: Prisma.NotificationUncheckedCreateInput) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BaseModel } from './base';
|
||||
export const UserSettingsSchema = z.object({
|
||||
receiveInvitationEmail: z.boolean().default(true),
|
||||
receiveMentionEmail: z.boolean().default(true),
|
||||
receiveCommentEmail: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type UserSettingsInput = z.input<typeof UserSettingsSchema>;
|
||||
|
||||
Reference in New Issue
Block a user