mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
@@ -0,0 +1,516 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import test from 'ava';
|
||||
|
||||
import {
|
||||
acceptInviteById,
|
||||
createTestingApp,
|
||||
createWorkspace,
|
||||
getNotificationCount,
|
||||
inviteUser,
|
||||
listNotifications,
|
||||
mentionUser,
|
||||
readNotification,
|
||||
TestingApp,
|
||||
} from '../../../__tests__/utils';
|
||||
import { Models, NotificationType } from '../../../models';
|
||||
import { MentionNotificationBodyType, NotificationObjectType } from '../types';
|
||||
|
||||
let app: TestingApp;
|
||||
let models: Models;
|
||||
|
||||
test.before(async () => {
|
||||
app = await createTestingApp();
|
||||
models = app.get(Models);
|
||||
});
|
||||
|
||||
test.after.always(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('should mention user in a doc', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
await models.workspace.update(workspace.id, {
|
||||
name: 'test-workspace-name',
|
||||
avatarKey: 'test-avatar-key',
|
||||
});
|
||||
const inviteId = await inviteUser(app, workspace.id, member.email);
|
||||
await app.switchUser(member);
|
||||
await acceptInviteById(app, workspace.id, inviteId);
|
||||
|
||||
await app.switchUser(owner);
|
||||
const mentionId = await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-1',
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
});
|
||||
t.truthy(mentionId);
|
||||
// mention user at another doc
|
||||
await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-2',
|
||||
title: 'doc-title-2',
|
||||
elementId: 'element-id-2',
|
||||
},
|
||||
});
|
||||
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
t.is(result.totalCount, 2);
|
||||
const notifications = result.edges.map(edge => edge.node);
|
||||
t.is(notifications.length, 2);
|
||||
|
||||
const notification = notifications[1] as NotificationObjectType;
|
||||
t.is(notification.read, false);
|
||||
t.truthy(notification.createdAt);
|
||||
t.truthy(notification.updatedAt);
|
||||
const body = notification.body as MentionNotificationBodyType;
|
||||
t.is(body.workspace!.id, workspace.id);
|
||||
t.is(body.doc.id, 'doc-id-1');
|
||||
t.is(body.doc.title, 'doc-title-1');
|
||||
t.is(body.doc.blockId, 'block-id-1');
|
||||
t.is(body.createdByUser!.id, owner.id);
|
||||
t.is(body.createdByUser!.name, owner.name);
|
||||
t.is(body.workspace!.id, workspace.id);
|
||||
t.is(body.workspace!.name, 'test-workspace-name');
|
||||
t.truthy(body.workspace!.avatarUrl);
|
||||
|
||||
const notification2 = notifications[0] as NotificationObjectType;
|
||||
t.is(notification2.read, false);
|
||||
t.truthy(notification2.createdAt);
|
||||
t.truthy(notification2.updatedAt);
|
||||
const body2 = notification2.body as MentionNotificationBodyType;
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.doc.id, 'doc-id-2');
|
||||
t.is(body2.doc.title, 'doc-title-2');
|
||||
t.is(body2.doc.elementId, 'element-id-2');
|
||||
t.is(body2.createdByUser!.id, owner.id);
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.workspace!.name, 'test-workspace-name');
|
||||
t.truthy(body2.workspace!.avatarUrl);
|
||||
});
|
||||
|
||||
test('should throw error when mention user has no Doc.Read role', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
await app.switchUser(owner);
|
||||
const docId = randomUUID();
|
||||
await t.throwsAsync(
|
||||
mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: docId,
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: `Mentioned user can not access doc ${docId}.`,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error when mention a not exists user', async t => {
|
||||
const owner = await app.signup();
|
||||
const workspace = await createWorkspace(app);
|
||||
await app.switchUser(owner);
|
||||
const docId = randomUUID();
|
||||
await t.throwsAsync(
|
||||
mentionUser(app, {
|
||||
userId: 'user-id-not-exists',
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: docId,
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: `Mentioned user can not access doc ${docId}.`,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should not mention user oneself', async t => {
|
||||
const owner = await app.signup();
|
||||
const workspace = await createWorkspace(app);
|
||||
await app.switchUser(owner);
|
||||
await t.throwsAsync(
|
||||
mentionUser(app, {
|
||||
userId: owner.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-1',
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: 'You can not mention yourself.',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should mark notification as read', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
const inviteId = await inviteUser(app, workspace.id, member.email);
|
||||
await app.switchUser(member);
|
||||
await acceptInviteById(app, workspace.id, inviteId);
|
||||
|
||||
await app.switchUser(owner);
|
||||
const mentionId = await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-1',
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
});
|
||||
t.truthy(mentionId);
|
||||
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
t.is(result.totalCount, 1);
|
||||
|
||||
const notifications = result.edges.map(edge => edge.node);
|
||||
const notification = notifications[0] as NotificationObjectType;
|
||||
t.is(notification.read, false);
|
||||
|
||||
await readNotification(app, notification.id);
|
||||
|
||||
const count = await getNotificationCount(app);
|
||||
t.is(count, 0);
|
||||
|
||||
// read again should work
|
||||
await readNotification(app, notification.id);
|
||||
});
|
||||
|
||||
test('should throw error when read the other user notification', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
const inviteId = await inviteUser(app, workspace.id, member.email);
|
||||
await app.switchUser(member);
|
||||
await acceptInviteById(app, workspace.id, inviteId);
|
||||
|
||||
await app.switchUser(owner);
|
||||
const mentionId = await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-1',
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
});
|
||||
t.truthy(mentionId);
|
||||
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
const notifications = result.edges.map(edge => edge.node);
|
||||
const notification = notifications[0] as NotificationObjectType;
|
||||
t.is(notification.read, false);
|
||||
|
||||
await app.switchUser(owner);
|
||||
await t.throwsAsync(readNotification(app, notification.id), {
|
||||
message: 'Notification not found.',
|
||||
});
|
||||
// notification not exists
|
||||
await t.throwsAsync(readNotification(app, 'notification-id-not-exists'), {
|
||||
message: 'Notification not found.',
|
||||
});
|
||||
});
|
||||
|
||||
test.skip('should throw error when mention call with invalid params', async t => {
|
||||
const owner = await app.signup();
|
||||
await app.switchUser(owner);
|
||||
await t.throwsAsync(
|
||||
mentionUser(app, {
|
||||
userId: '',
|
||||
workspaceId: '',
|
||||
doc: {
|
||||
id: '',
|
||||
title: '',
|
||||
blockId: '',
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: 'Mention user not found.',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should list and count notifications', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
{
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
const notifications = result.edges.map(edge => edge.node);
|
||||
t.is(notifications.length, 0);
|
||||
t.is(result.totalCount, 0);
|
||||
}
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
await models.workspace.update(workspace.id, {
|
||||
name: 'test-workspace-name1',
|
||||
avatarKey: 'test-avatar-key1',
|
||||
});
|
||||
const inviteId = await inviteUser(app, workspace.id, member.email);
|
||||
const workspace2 = await createWorkspace(app);
|
||||
await models.workspace.update(workspace2.id, {
|
||||
name: 'test-workspace-name2',
|
||||
avatarKey: 'test-avatar-key2',
|
||||
});
|
||||
const inviteId2 = await inviteUser(app, workspace2.id, member.email);
|
||||
await app.switchUser(member);
|
||||
await acceptInviteById(app, workspace.id, inviteId);
|
||||
await acceptInviteById(app, workspace2.id, inviteId2);
|
||||
|
||||
await app.switchUser(owner);
|
||||
await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-1',
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
},
|
||||
});
|
||||
await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-2',
|
||||
title: 'doc-title-2',
|
||||
blockId: 'block-id-2',
|
||||
},
|
||||
});
|
||||
await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-3',
|
||||
title: 'doc-title-3',
|
||||
blockId: 'block-id-3',
|
||||
},
|
||||
});
|
||||
// mention user in another workspace
|
||||
await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace2.id,
|
||||
doc: {
|
||||
id: 'doc-id-4',
|
||||
title: 'doc-title-4',
|
||||
blockId: 'block-id-4',
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
const notifications = result.edges.map(
|
||||
edge => edge.node
|
||||
) as NotificationObjectType[];
|
||||
t.is(notifications.length, 4);
|
||||
t.is(result.totalCount, 4);
|
||||
|
||||
const notification = notifications[0];
|
||||
t.is(notification.read, false);
|
||||
const body = notification.body as MentionNotificationBodyType;
|
||||
t.is(body.type, NotificationType.Mention);
|
||||
t.is(body.workspace!.id, workspace2.id);
|
||||
t.is(body.doc.id, 'doc-id-4');
|
||||
t.is(body.doc.title, 'doc-title-4');
|
||||
t.is(body.doc.blockId, 'block-id-4');
|
||||
t.is(body.createdByUser!.id, owner.id);
|
||||
t.is(body.workspace!.id, workspace2.id);
|
||||
t.is(body.workspace!.name, 'test-workspace-name2');
|
||||
t.truthy(body.workspace!.avatarUrl);
|
||||
|
||||
const notification2 = notifications[1];
|
||||
t.is(notification2.read, false);
|
||||
const body2 = notification2.body as MentionNotificationBodyType;
|
||||
t.is(body2.type, NotificationType.Mention);
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.doc.id, 'doc-id-3');
|
||||
t.is(body2.doc.title, 'doc-title-3');
|
||||
t.is(body2.doc.blockId, 'block-id-3');
|
||||
t.is(body2.createdByUser!.id, owner.id);
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.workspace!.name, 'test-workspace-name1');
|
||||
t.truthy(body2.workspace!.avatarUrl);
|
||||
}
|
||||
|
||||
{
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 10,
|
||||
offset: 2,
|
||||
});
|
||||
t.is(result.totalCount, 4);
|
||||
t.is(result.pageInfo.hasNextPage, false);
|
||||
t.is(result.pageInfo.hasPreviousPage, true);
|
||||
const notifications = result.edges.map(
|
||||
edge => edge.node
|
||||
) as NotificationObjectType[];
|
||||
t.is(notifications.length, 2);
|
||||
|
||||
const notification = notifications[0];
|
||||
t.is(notification.read, false);
|
||||
const body = notification.body as MentionNotificationBodyType;
|
||||
t.is(body.workspace!.id, workspace.id);
|
||||
t.is(body.doc.id, 'doc-id-2');
|
||||
t.is(body.doc.title, 'doc-title-2');
|
||||
t.is(body.doc.blockId, 'block-id-2');
|
||||
t.is(body.createdByUser!.id, owner.id);
|
||||
t.is(body.workspace!.id, workspace.id);
|
||||
t.is(body.workspace!.name, 'test-workspace-name1');
|
||||
t.truthy(body.workspace!.avatarUrl);
|
||||
|
||||
const notification2 = notifications[1];
|
||||
t.is(notification2.read, false);
|
||||
const body2 = notification2.body as MentionNotificationBodyType;
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.doc.id, 'doc-id-1');
|
||||
t.is(body2.doc.title, 'doc-title-1');
|
||||
t.is(body2.doc.blockId, 'block-id-1');
|
||||
t.is(body2.createdByUser!.id, owner.id);
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.workspace!.name, 'test-workspace-name1');
|
||||
t.truthy(body2.workspace!.avatarUrl);
|
||||
}
|
||||
|
||||
{
|
||||
await app.switchUser(member);
|
||||
const result = await listNotifications(app, {
|
||||
first: 2,
|
||||
offset: 0,
|
||||
});
|
||||
t.is(result.totalCount, 4);
|
||||
t.is(result.pageInfo.hasNextPage, true);
|
||||
t.is(result.pageInfo.hasPreviousPage, false);
|
||||
const notifications = result.edges.map(
|
||||
edge => edge.node
|
||||
) as NotificationObjectType[];
|
||||
t.is(notifications.length, 2);
|
||||
|
||||
const notification = notifications[0];
|
||||
t.is(notification.read, false);
|
||||
const body = notification.body as MentionNotificationBodyType;
|
||||
t.is(body.workspace!.id, workspace2.id);
|
||||
t.is(body.doc.id, 'doc-id-4');
|
||||
t.is(body.doc.title, 'doc-title-4');
|
||||
t.is(body.doc.blockId, 'block-id-4');
|
||||
t.is(body.createdByUser!.id, owner.id);
|
||||
t.is(body.workspace!.id, workspace2.id);
|
||||
t.is(body.workspace!.name, 'test-workspace-name2');
|
||||
t.truthy(body.workspace!.avatarUrl);
|
||||
t.is(
|
||||
notification.createdAt.toString(),
|
||||
Buffer.from(result.pageInfo.startCursor!, 'base64').toString('utf-8')
|
||||
);
|
||||
const notification2 = notifications[1];
|
||||
t.is(notification2.read, false);
|
||||
const body2 = notification2.body as MentionNotificationBodyType;
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.doc.id, 'doc-id-3');
|
||||
t.is(body2.doc.title, 'doc-title-3');
|
||||
t.is(body2.doc.blockId, 'block-id-3');
|
||||
t.is(body2.createdByUser!.id, owner.id);
|
||||
t.is(body2.workspace!.id, workspace.id);
|
||||
t.is(body2.workspace!.name, 'test-workspace-name1');
|
||||
t.truthy(body2.workspace!.avatarUrl);
|
||||
|
||||
await app.switchUser(owner);
|
||||
await mentionUser(app, {
|
||||
userId: member.id,
|
||||
workspaceId: workspace.id,
|
||||
doc: {
|
||||
id: 'doc-id-5',
|
||||
title: 'doc-title-5',
|
||||
blockId: 'block-id-5',
|
||||
},
|
||||
});
|
||||
|
||||
// get new notifications
|
||||
await app.switchUser(member);
|
||||
const result2 = await listNotifications(app, {
|
||||
first: 2,
|
||||
offset: 0,
|
||||
after: result.pageInfo.startCursor,
|
||||
});
|
||||
t.is(result2.totalCount, 5);
|
||||
t.is(result2.pageInfo.hasNextPage, false);
|
||||
t.is(result2.pageInfo.hasPreviousPage, true);
|
||||
const notifications2 = result2.edges.map(
|
||||
edge => edge.node
|
||||
) as NotificationObjectType[];
|
||||
t.is(notifications2.length, 1);
|
||||
|
||||
const notification3 = notifications2[0];
|
||||
t.is(notification3.read, false);
|
||||
const body3 = notification3.body as MentionNotificationBodyType;
|
||||
t.is(body3.workspace!.id, workspace.id);
|
||||
t.is(body3.doc.id, 'doc-id-5');
|
||||
t.is(body3.doc.title, 'doc-title-5');
|
||||
t.is(body3.doc.blockId, 'block-id-5');
|
||||
t.is(body3.createdByUser!.id, owner.id);
|
||||
t.is(body3.createdByUser!.name, owner.name);
|
||||
t.is(body3.workspace!.id, workspace.id);
|
||||
t.is(body3.workspace!.name, 'test-workspace-name1');
|
||||
t.truthy(body3.workspace!.avatarUrl);
|
||||
|
||||
// no new notifications
|
||||
const result3 = await listNotifications(app, {
|
||||
first: 2,
|
||||
offset: 0,
|
||||
after: result2.pageInfo.startCursor,
|
||||
});
|
||||
t.is(result3.totalCount, 5);
|
||||
t.is(result3.pageInfo.hasNextPage, false);
|
||||
t.is(result3.pageInfo.hasPreviousPage, true);
|
||||
t.is(result3.pageInfo.startCursor, null);
|
||||
t.is(result3.pageInfo.endCursor, null);
|
||||
t.is(result3.edges.length, 0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mock } from 'node:test';
|
||||
|
||||
import ava, { TestFn } from 'ava';
|
||||
|
||||
import {
|
||||
createTestingModule,
|
||||
type TestingModule,
|
||||
} from '../../../__tests__/utils';
|
||||
import { NotificationNotFound } from '../../../base';
|
||||
import {
|
||||
MentionNotificationBody,
|
||||
Models,
|
||||
NotificationType,
|
||||
User,
|
||||
Workspace,
|
||||
} from '../../../models';
|
||||
import { DocReader } from '../../doc';
|
||||
import { NotificationService } from '../service';
|
||||
interface Context {
|
||||
module: TestingModule;
|
||||
notificationService: NotificationService;
|
||||
models: Models;
|
||||
docReader: DocReader;
|
||||
}
|
||||
|
||||
const test = ava as TestFn<Context>;
|
||||
|
||||
test.before(async t => {
|
||||
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);
|
||||
});
|
||||
|
||||
let owner: User;
|
||||
let member: User;
|
||||
let workspace: Workspace;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await t.context.module.initTestingDB();
|
||||
owner = await t.context.models.user.create({
|
||||
email: `${randomUUID()}@affine.pro`,
|
||||
});
|
||||
member = await t.context.models.user.create({
|
||||
email: `${randomUUID()}@affine.pro`,
|
||||
});
|
||||
workspace = await t.context.models.workspace.create(owner.id);
|
||||
await t.context.models.workspace.update(workspace.id, {
|
||||
name: 'Test Workspace',
|
||||
avatarKey: 'test-avatar-key',
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach.always(() => {
|
||||
mock.reset();
|
||||
mock.timers.reset();
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
test('should create invitation notification', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const inviteId = randomUUID();
|
||||
const notification = await notificationService.createInvitation({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(notification);
|
||||
t.is(notification!.type, NotificationType.Invitation);
|
||||
t.is(notification!.userId, member.id);
|
||||
t.is(notification!.body.workspaceId, workspace.id);
|
||||
t.is(notification!.body.createdByUserId, owner.id);
|
||||
t.is(notification!.body.inviteId, inviteId);
|
||||
});
|
||||
|
||||
test('should not create invitation notification if user is already a member', async t => {
|
||||
const { notificationService, models } = t.context;
|
||||
const inviteId = randomUUID();
|
||||
mock.method(models.workspaceUser, 'getActive', async () => ({
|
||||
id: inviteId,
|
||||
}));
|
||||
const notification = await notificationService.createInvitation({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.is(notification, undefined);
|
||||
});
|
||||
|
||||
test('should create invitation accepted notification', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const inviteId = randomUUID();
|
||||
const notification = await notificationService.createInvitationAccepted({
|
||||
userId: owner.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: member.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(notification);
|
||||
t.is(notification!.type, NotificationType.InvitationAccepted);
|
||||
t.is(notification!.userId, owner.id);
|
||||
t.is(notification!.body.workspaceId, workspace.id);
|
||||
t.is(notification!.body.createdByUserId, member.id);
|
||||
t.is(notification!.body.inviteId, inviteId);
|
||||
});
|
||||
|
||||
test('should create invitation blocked notification', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const inviteId = randomUUID();
|
||||
const notification = await notificationService.createInvitationBlocked({
|
||||
userId: owner.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: member.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(notification);
|
||||
t.is(notification!.type, NotificationType.InvitationBlocked);
|
||||
t.is(notification!.userId, owner.id);
|
||||
t.is(notification!.body.workspaceId, workspace.id);
|
||||
t.is(notification!.body.createdByUserId, member.id);
|
||||
t.is(notification!.body.inviteId, inviteId);
|
||||
});
|
||||
|
||||
test('should create invitation rejected notification', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const inviteId = randomUUID();
|
||||
const notification = await notificationService.createInvitationRejected({
|
||||
userId: owner.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: member.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(notification);
|
||||
t.is(notification!.type, NotificationType.InvitationRejected);
|
||||
t.is(notification!.userId, owner.id);
|
||||
t.is(notification!.body.workspaceId, workspace.id);
|
||||
t.is(notification!.body.createdByUserId, member.id);
|
||||
t.is(notification!.body.inviteId, inviteId);
|
||||
});
|
||||
|
||||
test('should clean expired notifications', async t => {
|
||||
const { notificationService } = t.context;
|
||||
await notificationService.createInvitation({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
inviteId: randomUUID(),
|
||||
},
|
||||
});
|
||||
let count = await notificationService.countByUserId(member.id);
|
||||
t.is(count, 1);
|
||||
// wait for 100 days
|
||||
mock.timers.enable({
|
||||
apis: ['Date'],
|
||||
now: Date.now() + 1000 * 60 * 60 * 24 * 100,
|
||||
});
|
||||
await t.context.models.notification.cleanExpiredNotifications();
|
||||
count = await notificationService.countByUserId(member.id);
|
||||
t.is(count, 1);
|
||||
mock.timers.reset();
|
||||
// wait for 1 year
|
||||
mock.timers.enable({
|
||||
apis: ['Date'],
|
||||
now: Date.now() + 1000 * 60 * 60 * 24 * 365,
|
||||
});
|
||||
await t.context.models.notification.cleanExpiredNotifications();
|
||||
count = await notificationService.countByUserId(member.id);
|
||||
t.is(count, 0);
|
||||
});
|
||||
|
||||
test('should mark notification as read', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const notification = await notificationService.createInvitation({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
inviteId: randomUUID(),
|
||||
},
|
||||
});
|
||||
await notificationService.markAsRead(member.id, notification!.id);
|
||||
const updatedNotification = await t.context.models.notification.get(
|
||||
notification!.id
|
||||
);
|
||||
t.is(updatedNotification!.read, true);
|
||||
});
|
||||
|
||||
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()), {
|
||||
instanceOf: NotificationNotFound,
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw error on mark notification as read if notification user is not the same', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const notification = await notificationService.createInvitation({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
inviteId: randomUUID(),
|
||||
},
|
||||
});
|
||||
const otherUser = await t.context.models.user.create({
|
||||
email: `${randomUUID()}@affine.pro`,
|
||||
});
|
||||
await t.throwsAsync(
|
||||
notificationService.markAsRead(otherUser.id, notification!.id),
|
||||
{
|
||||
instanceOf: NotificationNotFound,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should use latest doc title in mention notification', async t => {
|
||||
const { notificationService, models } = t.context;
|
||||
const docId = randomUUID();
|
||||
await notificationService.createMention({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
doc: { id: docId, title: 'doc-title-1', blockId: 'block-id-1' },
|
||||
},
|
||||
});
|
||||
const mentionNotification = await notificationService.createMention({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
doc: { id: docId, title: 'doc-title-2', blockId: 'block-id-2' },
|
||||
},
|
||||
});
|
||||
t.truthy(mentionNotification);
|
||||
mock.method(models.doc, 'findMetas', async () => [
|
||||
{
|
||||
title: 'doc-title-2-updated',
|
||||
},
|
||||
{
|
||||
title: 'doc-title-1-updated',
|
||||
},
|
||||
]);
|
||||
const notifications = await notificationService.findManyByUserId(member.id);
|
||||
t.is(notifications.length, 2);
|
||||
const mention = notifications[0];
|
||||
t.is(mention.body.workspace!.id, workspace.id);
|
||||
t.is(mention.body.workspace!.name, 'Test Workspace');
|
||||
t.truthy(mention.body.workspace!.avatarUrl);
|
||||
t.is(mention.body.type, NotificationType.Mention);
|
||||
const body = mention.body as MentionNotificationBody;
|
||||
t.is(body.doc.title, 'doc-title-2-updated');
|
||||
|
||||
const mention2 = notifications[1];
|
||||
t.is(mention2.body.workspace!.id, workspace.id);
|
||||
t.is(mention2.body.workspace!.name, 'Test Workspace');
|
||||
t.truthy(mention2.body.workspace!.avatarUrl);
|
||||
t.is(mention2.body.type, NotificationType.Mention);
|
||||
const body2 = mention2.body as MentionNotificationBody;
|
||||
t.is(body2.doc.title, 'doc-title-1-updated');
|
||||
});
|
||||
|
||||
test('should raw doc title in mention notification if no doc found', async t => {
|
||||
const { notificationService, models } = t.context;
|
||||
const docId = randomUUID();
|
||||
await notificationService.createMention({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
doc: { id: docId, title: 'doc-title-1', blockId: 'block-id-1' },
|
||||
},
|
||||
});
|
||||
await notificationService.createMention({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
doc: { id: docId, title: 'doc-title-2', blockId: 'block-id-2' },
|
||||
},
|
||||
});
|
||||
mock.method(models.doc, 'findMetas', async () => [null, null]);
|
||||
const notifications = await notificationService.findManyByUserId(member.id);
|
||||
t.is(notifications.length, 2);
|
||||
const mention = notifications[0];
|
||||
t.is(mention.body.workspace!.name, 'Test Workspace');
|
||||
t.is(mention.body.type, NotificationType.Mention);
|
||||
const body = mention.body as MentionNotificationBody;
|
||||
t.is(body.doc.title, 'doc-title-2');
|
||||
|
||||
const mention2 = notifications[1];
|
||||
t.is(mention2.body.workspace!.name, 'Test Workspace');
|
||||
t.is(mention2.body.type, NotificationType.Mention);
|
||||
const body2 = mention2.body as MentionNotificationBody;
|
||||
t.is(body2.doc.title, 'doc-title-1');
|
||||
});
|
||||
20
packages/backend/server/src/core/notification/index.ts
Normal file
20
packages/backend/server/src/core/notification/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DocStorageModule } from '../doc';
|
||||
import { PermissionModule } from '../permission';
|
||||
import { StorageModule } from '../storage';
|
||||
import { NotificationJob } from './job';
|
||||
import { NotificationResolver, UserNotificationResolver } from './resolver';
|
||||
import { NotificationService } from './service';
|
||||
|
||||
@Module({
|
||||
imports: [PermissionModule, DocStorageModule, StorageModule],
|
||||
providers: [
|
||||
UserNotificationResolver,
|
||||
NotificationResolver,
|
||||
NotificationService,
|
||||
NotificationJob,
|
||||
],
|
||||
exports: [NotificationService],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
35
packages/backend/server/src/core/notification/job.ts
Normal file
35
packages/backend/server/src/core/notification/job.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
|
||||
import { JobQueue, OnJob } from '../../base';
|
||||
import { NotificationService } from './service';
|
||||
|
||||
declare global {
|
||||
interface Jobs {
|
||||
'nightly.cleanExpiredNotifications': {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NotificationJob {
|
||||
constructor(
|
||||
private readonly service: NotificationService,
|
||||
private readonly queue: JobQueue
|
||||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||
async nightlyJob() {
|
||||
await this.queue.add(
|
||||
'nightly.cleanExpiredNotifications',
|
||||
{},
|
||||
{
|
||||
jobId: 'nightly-notification-clean-expired',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@OnJob('nightly.cleanExpiredNotifications')
|
||||
async cleanExpiredNotifications() {
|
||||
await this.service.cleanExpiredNotifications();
|
||||
}
|
||||
}
|
||||
114
packages/backend/server/src/core/notification/resolver.ts
Normal file
114
packages/backend/server/src/core/notification/resolver.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
Args,
|
||||
ID,
|
||||
Int,
|
||||
Mutation,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
MentionUserDocAccessDenied,
|
||||
MentionUserOneselfDenied,
|
||||
} from '../../base/error';
|
||||
import { paginate, PaginationInput } from '../../base/graphql';
|
||||
import { MentionNotificationCreateSchema } from '../../models';
|
||||
import { CurrentUser } from '../auth/session';
|
||||
import { AccessController } from '../permission';
|
||||
import { UserType } from '../user';
|
||||
import { NotificationService } from './service';
|
||||
import {
|
||||
MentionInput,
|
||||
NotificationObjectType,
|
||||
PaginatedNotificationObjectType,
|
||||
UnionNotificationBodyType,
|
||||
} from './types';
|
||||
|
||||
@Resolver(() => UserType)
|
||||
export class UserNotificationResolver {
|
||||
constructor(
|
||||
private readonly service: NotificationService,
|
||||
private readonly ac: AccessController
|
||||
) {}
|
||||
|
||||
@ResolveField(() => PaginatedNotificationObjectType, {
|
||||
description: 'Get current user notifications',
|
||||
})
|
||||
async notifications(
|
||||
@CurrentUser() me: UserType,
|
||||
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
|
||||
): Promise<PaginatedNotificationObjectType> {
|
||||
const [notifications, totalCount] = await Promise.all([
|
||||
this.service.findManyByUserId(me.id, pagination),
|
||||
this.service.countByUserId(me.id),
|
||||
]);
|
||||
return paginate(notifications, 'createdAt', pagination, totalCount);
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'Get user notification count',
|
||||
})
|
||||
async notificationCount(@CurrentUser() me: UserType): Promise<number> {
|
||||
return await this.service.countByUserId(me.id);
|
||||
}
|
||||
|
||||
@Mutation(() => ID, {
|
||||
description: 'mention user in a doc',
|
||||
})
|
||||
async mentionUser(
|
||||
@CurrentUser() me: UserType,
|
||||
@Args('input') input: MentionInput
|
||||
) {
|
||||
const parsedInput = MentionNotificationCreateSchema.parse({
|
||||
userId: input.userId,
|
||||
body: {
|
||||
workspaceId: input.workspaceId,
|
||||
doc: input.doc,
|
||||
createdByUserId: me.id,
|
||||
},
|
||||
});
|
||||
if (parsedInput.userId === me.id) {
|
||||
throw new MentionUserOneselfDenied();
|
||||
}
|
||||
// currentUser can update the doc
|
||||
await this.ac
|
||||
.user(me.id)
|
||||
.doc(parsedInput.body.workspaceId, parsedInput.body.doc.id)
|
||||
.assert('Doc.Update');
|
||||
// mention user can read the doc
|
||||
if (
|
||||
!(await this.ac
|
||||
.user(parsedInput.userId)
|
||||
.doc(parsedInput.body.workspaceId, parsedInput.body.doc.id)
|
||||
.can('Doc.Read'))
|
||||
) {
|
||||
throw new MentionUserDocAccessDenied({
|
||||
docId: parsedInput.body.doc.id,
|
||||
});
|
||||
}
|
||||
const notification = await this.service.createMention(parsedInput);
|
||||
return notification.id;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'mark notification as read',
|
||||
})
|
||||
async readNotification(
|
||||
@CurrentUser() me: UserType,
|
||||
@Args('id') notificationId: string
|
||||
) {
|
||||
await this.service.markAsRead(me.id, notificationId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver(() => NotificationObjectType)
|
||||
export class NotificationResolver {
|
||||
@ResolveField(() => UnionNotificationBodyType, {
|
||||
description:
|
||||
"Just a placeholder to export UnionNotificationBodyType, don't use it",
|
||||
})
|
||||
async _placeholderForUnionNotificationBodyType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
171
packages/backend/server/src/core/notification/service.ts
Normal file
171
packages/backend/server/src/core/notification/service.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||
|
||||
import { NotificationNotFound, PaginationInput } from '../../base';
|
||||
import {
|
||||
InvitationNotificationCreate,
|
||||
MentionNotification,
|
||||
MentionNotificationCreate,
|
||||
Models,
|
||||
NotificationType,
|
||||
UnionNotificationBody,
|
||||
} from '../../models';
|
||||
import { DocReader } from '../doc';
|
||||
import { WorkspaceBlobStorage } from '../storage';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
private readonly logger = new Logger(NotificationService.name);
|
||||
|
||||
constructor(
|
||||
private readonly models: Models,
|
||||
private readonly docReader: DocReader,
|
||||
private readonly workspaceBlobStorage: WorkspaceBlobStorage
|
||||
) {}
|
||||
|
||||
async cleanExpiredNotifications() {
|
||||
return await this.models.notification.cleanExpiredNotifications();
|
||||
}
|
||||
|
||||
async createMention(input: MentionNotificationCreate) {
|
||||
return await this.models.notification.createMention(input);
|
||||
}
|
||||
|
||||
async createInvitation(input: InvitationNotificationCreate) {
|
||||
const isActive = await this.models.workspaceUser.getActive(
|
||||
input.body.workspaceId,
|
||||
input.userId
|
||||
);
|
||||
if (isActive) {
|
||||
this.logger.debug(
|
||||
`User ${input.userId} is already a active member of workspace ${input.body.workspaceId}, skip creating notification`
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.ensureWorkspaceContentExists(input.body.workspaceId);
|
||||
return await this.models.notification.createInvitation(
|
||||
input,
|
||||
NotificationType.Invitation
|
||||
);
|
||||
}
|
||||
|
||||
async createInvitationAccepted(input: InvitationNotificationCreate) {
|
||||
await this.ensureWorkspaceContentExists(input.body.workspaceId);
|
||||
return await this.models.notification.createInvitation(
|
||||
input,
|
||||
NotificationType.InvitationAccepted
|
||||
);
|
||||
}
|
||||
|
||||
async createInvitationBlocked(input: InvitationNotificationCreate) {
|
||||
await this.ensureWorkspaceContentExists(input.body.workspaceId);
|
||||
return await this.models.notification.createInvitation(
|
||||
input,
|
||||
NotificationType.InvitationBlocked
|
||||
);
|
||||
}
|
||||
|
||||
async createInvitationRejected(input: InvitationNotificationCreate) {
|
||||
await this.ensureWorkspaceContentExists(input.body.workspaceId);
|
||||
return await this.models.notification.createInvitation(
|
||||
input,
|
||||
NotificationType.InvitationRejected
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureWorkspaceContentExists(workspaceId: string) {
|
||||
const workspace = await this.models.workspace.get(workspaceId);
|
||||
if (!workspace || workspace.name) {
|
||||
return;
|
||||
}
|
||||
const content = await this.docReader.getWorkspaceContent(workspaceId);
|
||||
if (!content?.name) {
|
||||
return;
|
||||
}
|
||||
await this.models.workspace.update(workspaceId, {
|
||||
name: content.name,
|
||||
avatarKey: content.avatarKey,
|
||||
});
|
||||
}
|
||||
|
||||
async markAsRead(userId: string, notificationId: string) {
|
||||
try {
|
||||
await this.models.notification.markAsRead(notificationId, userId);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof PrismaClientKnownRequestError &&
|
||||
err.code === 'P2025'
|
||||
) {
|
||||
// https://www.prisma.io/docs/orm/reference/error-reference#p2025
|
||||
throw new NotificationNotFound();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find notifications by user id, order by createdAt desc
|
||||
*/
|
||||
async findManyByUserId(userId: string, options?: PaginationInput) {
|
||||
const notifications = await this.models.notification.findManyByUserId(
|
||||
userId,
|
||||
options
|
||||
);
|
||||
|
||||
// fill user info
|
||||
const userIds = new Set(notifications.map(n => n.body.createdByUserId));
|
||||
const users = await this.models.user.getPublicUsers(Array.from(userIds));
|
||||
const userInfos = new Map(users.map(u => [u.id, u]));
|
||||
|
||||
// fill workspace info
|
||||
const workspaceIds = new Set(notifications.map(n => n.body.workspaceId));
|
||||
const workspaces = await this.models.workspace.findMany(
|
||||
Array.from(workspaceIds)
|
||||
);
|
||||
const workspaceInfos = new Map(
|
||||
workspaces.map(w => [
|
||||
w.id,
|
||||
{
|
||||
id: w.id,
|
||||
name: w.name ?? '',
|
||||
avatarUrl: w.avatarKey
|
||||
? this.workspaceBlobStorage.getAvatarUrl(w.id, w.avatarKey)
|
||||
: undefined,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
// fill latest doc title
|
||||
const mentions = notifications.filter(
|
||||
n => n.type === NotificationType.Mention
|
||||
) as MentionNotification[];
|
||||
const mentionDocs = await this.models.doc.findMetas(
|
||||
mentions.map(m => ({
|
||||
workspaceId: m.body.workspaceId,
|
||||
docId: m.body.doc.id,
|
||||
}))
|
||||
);
|
||||
for (const [index, mention] of mentions.entries()) {
|
||||
const doc = mentionDocs[index];
|
||||
if (doc?.title) {
|
||||
// use the latest doc title
|
||||
mention.body.doc.title = doc.title;
|
||||
}
|
||||
}
|
||||
|
||||
return notifications.map(n => ({
|
||||
...n,
|
||||
body: {
|
||||
...(n.body as UnionNotificationBody),
|
||||
// set type to body.type to improve type inference on frontend
|
||||
type: n.type,
|
||||
workspace: workspaceInfos.get(n.body.workspaceId),
|
||||
createdByUser: userInfos.get(n.body.createdByUserId),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async countByUserId(userId: string) {
|
||||
return await this.models.notification.countByUserId(userId);
|
||||
}
|
||||
}
|
||||
196
packages/backend/server/src/core/notification/types.ts
Normal file
196
packages/backend/server/src/core/notification/types.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
createUnionType,
|
||||
Field,
|
||||
ID,
|
||||
InputType,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
import { GraphQLJSONObject } from 'graphql-scalars';
|
||||
|
||||
import { Paginated } from '../../base';
|
||||
import {
|
||||
InvitationNotificationBody,
|
||||
Notification,
|
||||
NotificationLevel,
|
||||
NotificationType,
|
||||
} from '../../models';
|
||||
import { WorkspaceDocInfo } from '../doc/reader';
|
||||
import { PublicUserType } from '../user';
|
||||
|
||||
registerEnumType(NotificationLevel, {
|
||||
name: 'NotificationLevel',
|
||||
description: 'Notification level',
|
||||
});
|
||||
|
||||
registerEnumType(NotificationType, {
|
||||
name: 'NotificationType',
|
||||
description: 'Notification type',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class NotificationWorkspaceType implements WorkspaceDocInfo {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field({ description: 'Workspace name' })
|
||||
name!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Workspace avatar url',
|
||||
nullable: true,
|
||||
})
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export abstract class BaseNotificationBodyType {
|
||||
@Field(() => NotificationType, {
|
||||
description: 'The type of the notification',
|
||||
})
|
||||
type!: NotificationType;
|
||||
|
||||
@Field(() => PublicUserType, {
|
||||
nullable: true,
|
||||
description:
|
||||
'The user who created the notification, maybe null when user is deleted or sent by system',
|
||||
})
|
||||
createdByUser?: PublicUserType;
|
||||
|
||||
@Field(() => NotificationWorkspaceType, {
|
||||
nullable: true,
|
||||
})
|
||||
workspace?: NotificationWorkspaceType;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class MentionDocType {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: true,
|
||||
})
|
||||
blockId?: string;
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: true,
|
||||
})
|
||||
elementId?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class MentionNotificationBodyType extends BaseNotificationBodyType {
|
||||
@Field(() => MentionDocType)
|
||||
doc!: MentionDocType;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class InvitationNotificationBodyType
|
||||
extends BaseNotificationBodyType
|
||||
implements Partial<InvitationNotificationBody>
|
||||
{
|
||||
@Field(() => ID)
|
||||
inviteId!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class InvitationAcceptedNotificationBodyType
|
||||
extends BaseNotificationBodyType
|
||||
implements Partial<InvitationNotificationBody>
|
||||
{
|
||||
@Field(() => String)
|
||||
inviteId!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class InvitationBlockedNotificationBodyType
|
||||
extends BaseNotificationBodyType
|
||||
implements Partial<InvitationNotificationBody>
|
||||
{
|
||||
@Field(() => String)
|
||||
inviteId!: string;
|
||||
}
|
||||
|
||||
export const UnionNotificationBodyType = createUnionType({
|
||||
name: 'UnionNotificationBodyType',
|
||||
types: () =>
|
||||
[
|
||||
MentionNotificationBodyType,
|
||||
InvitationNotificationBodyType,
|
||||
InvitationAcceptedNotificationBodyType,
|
||||
InvitationBlockedNotificationBodyType,
|
||||
] as const,
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class NotificationObjectType implements Partial<Notification> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field(() => NotificationLevel, {
|
||||
description: 'The level of the notification',
|
||||
})
|
||||
level!: NotificationLevel;
|
||||
|
||||
@Field(() => NotificationType, {
|
||||
description: 'The type of the notification',
|
||||
})
|
||||
type!: NotificationType;
|
||||
|
||||
@Field({ description: 'Whether the notification has been read' })
|
||||
read!: boolean;
|
||||
|
||||
@Field({ description: 'The created at time of the notification' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Field({ description: 'The updated at time of the notification' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Field(() => GraphQLJSONObject, {
|
||||
description:
|
||||
'The body of the notification, different types have different fields, see UnionNotificationBodyType',
|
||||
})
|
||||
body!: object;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PaginatedNotificationObjectType extends Paginated(
|
||||
NotificationObjectType
|
||||
) {}
|
||||
|
||||
@InputType()
|
||||
export class MentionDocInput {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'The block id in the doc',
|
||||
nullable: true,
|
||||
})
|
||||
blockId?: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'The element id in the doc',
|
||||
nullable: true,
|
||||
})
|
||||
elementId?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class MentionInput {
|
||||
@Field()
|
||||
userId!: string;
|
||||
|
||||
@Field()
|
||||
workspaceId!: string;
|
||||
|
||||
@Field(() => MentionDocInput)
|
||||
doc!: MentionDocInput;
|
||||
}
|
||||
Reference in New Issue
Block a user