Skip to content

Commit 64d43fd

Browse files
committed
created backend for notifications
1 parent a241c83 commit 64d43fd

8 files changed

Lines changed: 322 additions & 2 deletions

File tree

apps/rpc/src/modules/core.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ import { getMembershipService } from "./user/membership-service"
5353
import { getUserRepository } from "./user/user-repository"
5454
import { getUserService } from "./user/user-service"
5555
import { getWorkspaceService } from "./workspace-sync/workspace-service"
56-
56+
import { getNotificationRepository } from "./notification/notification-repository"
57+
import { getNotificationService } from "./notification/notification-service"
5758
export type ServiceLayer = Awaited<ReturnType<typeof createServiceLayer>>
5859

5960
const WORKSPACE_SERVICE_ACCOUNT_SCOPES = [
@@ -154,6 +155,7 @@ export async function createServiceLayer(
154155
const feideGroupsRepository = getFeideGroupsRepository()
155156
const feedbackFormRepository = getFeedbackFormRepository()
156157
const feedbackFormAnswerRepository = getFeedbackFormAnswerRepository()
158+
const notificationRepository = getNotificationRepository()
157159

158160
const membershipService = getMembershipService()
159161
const emailService = isAmazonSesEmailFeatureEnabled(configuration)
@@ -184,6 +186,7 @@ export async function createServiceLayer(
184186
attendanceRepository
185187
)
186188
const feedbackFormAnswerService = getFeedbackFormAnswerService(feedbackFormAnswerRepository, feedbackFormService)
189+
const notificationService = getNotificationService(notificationRepository)
187190
const taskDiscoveryService = getLocalTaskDiscoveryService(clients.prisma, taskService, recurringTaskService)
188191
const attendanceService = getAttendanceService(
189192
eventEmitter,
@@ -248,6 +251,7 @@ export async function createServiceLayer(
248251
paymentWebhookService,
249252
recurringTaskService,
250253
workspaceService,
254+
notificationService,
251255
executeTransaction: clients.prisma.$transaction.bind(clients.prisma),
252256
// Do not use this directly, it is here for repl/script purposes only
253257
prisma: clients.prisma,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { DBHandle } from "@dotkomonline/db"
2+
import { Notification, NotificationId, NotificationWrite, UserId, NotificationRecipientId, NotificationRecipient } from "@dotkomonline/types"
3+
4+
export interface NotificationRepository {
5+
6+
findById(handle: DBHandle, notificationId: NotificationId): Promise<Notification | null>
7+
create(handle: DBHandle, notificationData: NotificationWrite): Promise<Notification>
8+
update(handle: DBHandle, notificationId: NotificationId, notificationData: Partial<NotificationWrite>): Promise<Notification>
9+
delete(handle: DBHandle, notificationId: NotificationId): Promise<Notification | null>
10+
11+
addRecipients(handle: DBHandle, notificationId: NotificationId, recipientIds: UserId[]): Promise<void>
12+
removeRecipients(handle: DBHandle, notificationId: NotificationId, recipientIds: UserId[]): Promise<void>
13+
14+
findRecipient(handle: DBHandle, recipientId: NotificationRecipientId, userId: UserId): Promise<NotificationRecipient | null>
15+
findAllforUser(handle: DBHandle, userId: UserId): Promise<Notification[]>
16+
getUnreadCountforUser(handle: DBHandle, userId: UserId): Promise<number>
17+
markAsRead(handle: DBHandle, notificationId: NotificationId, userId: UserId): Promise<void>
18+
markAllAsRead(handle: DBHandle, userId: UserId): Promise<void>
19+
}
20+
21+
export function getNotificationRepository(): NotificationRepository {
22+
return {
23+
async findById(handle, notificationId) {
24+
const notification = await handle.notification.findUnique({
25+
where: { id: notificationId },
26+
})
27+
return notification
28+
},
29+
30+
async create(handle, notificationData) {
31+
const { recipientIds, ...data } = notificationData
32+
const notification = await handle.notification.create({ data })
33+
return notification
34+
},
35+
async update(handle, notificationId, notificationData) {
36+
const { recipientIds, ...data } = notificationData
37+
const notification = await handle.notification.update({
38+
where: { id: notificationId },
39+
data,
40+
})
41+
return notification
42+
43+
},
44+
async delete(handle, notificationId) {
45+
const notification = await handle.notification.findUnique({
46+
where: { id: notificationId },
47+
})
48+
await handle.notification.delete({
49+
where: { id: notificationId },
50+
})
51+
return notification
52+
},
53+
async findRecipient(handle, recipientId, userId) {
54+
const recipient = await handle.notificationRecipient.findFirst({
55+
where: {
56+
id: recipientId,
57+
userId,
58+
},
59+
})
60+
return recipient
61+
},
62+
async addRecipients(handle, notificationId, recipientIds) {
63+
await handle.notificationRecipient.createMany({
64+
data: recipientIds.map((userId) => ({
65+
notificationId,
66+
userId,
67+
})),
68+
})
69+
},
70+
async removeRecipients(handle, notificationId, recipientIds) {
71+
await handle.notificationRecipient.deleteMany({
72+
where: {
73+
notificationId,
74+
userId: { in: recipientIds },
75+
},
76+
})
77+
},
78+
79+
async findAllForUser(handle, userId) {
80+
return handle.notificationRecipient.findMany({
81+
where: { userId },
82+
include: { notification: true },
83+
})
84+
},
85+
86+
async getUnreadCountforUser(handle, userId) {
87+
await handle.notificationRecipient.count({
88+
where: { userId, readAt: null },
89+
})
90+
},
91+
92+
async getUnreadCountForUser(handle, userId) {
93+
return handle.notificationRecipient.count({
94+
where: { userId, readAt: null },
95+
})
96+
},
97+
98+
async markAllAsRead(handle, userId) {
99+
await handle.notificationRecipient.updateMany({
100+
where: { userId, readAt: null },
101+
data: { readAt: new Date() },
102+
})
103+
},
104+
}
105+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { NotificationSchema, NotificationWriteSchema, UserNotificationSchema } from "@dotkomonline/types"
2+
import type { inferProcedureInput, inferProcedureOutput } from "@trpc/server"
3+
import { z } from "zod"
4+
import { isEditor } from "../../authorization"
5+
import { withAuditLogEntry, withAuthentication, withAuthorization, withDatabaseTransaction } from "../../middlewares"
6+
import { procedure, t } from "../../trpc"
7+
8+
export type GetNotificationInput = inferProcedureInput<typeof getNotificationProcedure>
9+
export type GetNotificationOutput = inferProcedureOutput<typeof getNotificationProcedure>
10+
const getNotificationProcedure = procedure
11+
.input(NotificationSchema.shape.id)
12+
.use(withDatabaseTransaction())
13+
.query(async ({ input, ctx }) => {
14+
return ctx.notificationService.findById(ctx.handle, input)
15+
})
16+
17+
export type CreateNotificationInput = inferProcedureInput<typeof createNotificationProcedure>
18+
export type CreateNotificationOutput = inferProcedureOutput<typeof createNotificationProcedure>
19+
const createNotificationProcedure = procedure
20+
.input(NotificationWriteSchema)
21+
.use(withAuthentication())
22+
.use(withAuthorization(isEditor()))
23+
.use(withDatabaseTransaction())
24+
.use(withAuditLogEntry())
25+
.mutation(async ({ input, ctx }) => {
26+
return ctx.notificationService.create(ctx.handle, input)
27+
})
28+
29+
export type EditNotificationInput = inferProcedureInput<typeof editNotificationProcedure>
30+
export type EditNotificationOutput = inferProcedureOutput<typeof editNotificationProcedure>
31+
const editNotificationProcedure = procedure
32+
.input(z.object({
33+
id: NotificationSchema.shape.id,
34+
input: NotificationWriteSchema.partial(),
35+
}))
36+
.use(withAuthentication())
37+
.use(withAuthorization(isEditor()))
38+
.use(withDatabaseTransaction())
39+
.use(withAuditLogEntry())
40+
.mutation(async ({ input: changes, ctx }) => {
41+
return ctx.notificationService.update(ctx.handle, changes.id, changes.input)
42+
})
43+
44+
export type DeleteNotificationInput = inferProcedureInput<typeof deleteNotificationProcedure>
45+
export type DeleteNotificationOutput = inferProcedureOutput<typeof deleteNotificationProcedure>
46+
const deleteNotificationProcedure = procedure
47+
.input(NotificationSchema.shape.id)
48+
.use(withAuthentication())
49+
.use(withAuthorization(isEditor()))
50+
.use(withDatabaseTransaction())
51+
.use(withAuditLogEntry())
52+
.mutation(async ({ input, ctx }) => {
53+
return ctx.notificationService.delete(ctx.handle, input)
54+
})
55+
56+
export type GetMyNotificationsInput = inferProcedureInput<typeof getMyNotificationsProcedure>
57+
export type GetMyNotificationsOutput = inferProcedureOutput<typeof getMyNotificationsProcedure>
58+
const getMyNotificationsProcedure = procedure
59+
.use(withAuthentication())
60+
.use(withDatabaseTransaction())
61+
.query(async ({ ctx }) => {
62+
return ctx.notificationService.findAllForUser(ctx.handle, ctx.principal.subject)
63+
})
64+
65+
export type GetUnreadCountInput = inferProcedureInput<typeof getUnreadCountProcedure>
66+
export type GetUnreadCountOutput = inferProcedureOutput<typeof getUnreadCountProcedure>
67+
const getUnreadCountProcedure = procedure
68+
.use(withAuthentication())
69+
.use(withDatabaseTransaction())
70+
.query(async ({ ctx }) => {
71+
return ctx.notificationService.getUnreadCountForUser(ctx.handle, ctx.principal.subject)
72+
})
73+
74+
export type MarkAsReadInput = inferProcedureInput<typeof markAsReadProcedure>
75+
export type MarkAsReadOutput = inferProcedureOutput<typeof markAsReadProcedure>
76+
const markAsReadProcedure = procedure
77+
.input(z.object({ notificationId: NotificationSchema.shape.id }))
78+
.use(withAuthentication())
79+
.use(withDatabaseTransaction())
80+
.mutation(async ({ input, ctx }) => {
81+
return ctx.notificationService.markAsRead(ctx.handle, input.notificationId, ctx.principal.subject)
82+
})
83+
84+
export type MarkAllAsReadInput = inferProcedureInput<typeof markAllAsReadProcedure>
85+
export type MarkAllAsReadOutput = inferProcedureOutput<typeof markAllAsReadProcedure>
86+
const markAllAsReadProcedure = procedure
87+
.use(withAuthentication())
88+
.use(withDatabaseTransaction())
89+
.mutation(async ({ ctx }) => {
90+
return ctx.notificationService.markAllAsRead(ctx.handle, ctx.principal.subject)
91+
})
92+
93+
export const notificationRouter = t.router({
94+
get: getNotificationProcedure,
95+
create: createNotificationProcedure,
96+
edit: editNotificationProcedure,
97+
delete: deleteNotificationProcedure,
98+
getMyNotifications: getMyNotificationsProcedure,
99+
getUnreadCount: getUnreadCountProcedure,
100+
markAsRead: markAsReadProcedure,
101+
markAllAsRead: markAllAsReadProcedure,
102+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { DBHandle } from "@dotkomonline/db"
2+
import { NotFoundError } from "../../error"
3+
import type { Notification, NotificationId } from "@dotkomonline/types"
4+
import { NotificationRepository } from "./notification-repository"
5+
6+
export interface NotificationService {
7+
findById(handle: DBHandle, notificationId: NotificationId): Promise<Notification | null>
8+
create(handle: DBHandle, notificationData: NotificationWrite): Promise<Notification>
9+
update(handle: DBHandle, notificationId: NotificationId, notificationData: Partial<NotificationWrite>): Promise<Notification>
10+
delete(handle: DBHandle, notificationId: NotificationId): Promise<Notification | null>
11+
12+
addRecipients(handle: DBHandle, notificationId: NotificationId, recipientIds: UserId[]): Promise<void>
13+
removeRecipients(handle: DBHandle, notificationId: NotificationId, recipientIds: UserId[]): Promise<void>
14+
15+
findRecipient(handle: DBHandle, recipientId: NotificationRecipientId, userId: UserId): Promise<NotificationRecipient | null>
16+
findAllforUser(handle: DBHandle, userId: UserId): Promise<Notification[]>
17+
getUnreadCountforUser(handle: DBHandle, userId: UserId): Promise<number>
18+
markAsRead(handle: DBHandle, notificationId: NotificationId, userId: UserId): Promise<void>
19+
markAllAsRead(handle: DBHandle, userId: UserId): Promise<void>
20+
}
21+
22+
export function getNotificationService(notificationRepository: NotificationRepository): NotificationService {
23+
return {
24+
async findById(handle, notificationId) {
25+
return await notificationRepository.findById(handle, notificationId)
26+
},
27+
async create(handle, notificationData) {
28+
return await notificationRepository.create(handle, notificationData)
29+
},
30+
31+
async update(handle, notificationId, notificationData) {
32+
const notification = await notificationRepository.findById(handle, notificationId)
33+
return await notificationRepository.update(handle, notificationId, notificationData)
34+
},
35+
36+
async delete(handle, notificationId) {
37+
return await notificationRepository.delete(handle, notificationId)
38+
},
39+
40+
async addRecipients(handle, notificationId, recipientIds) {
41+
await notificationRepository.addRecipients(handle, notificationId, recipientIds)
42+
},
43+
44+
async removeRecipients(handle, notificationId, recipientIds) {
45+
await notificationRepository.removeRecipients(handle, notificationId, recipientIds)
46+
},
47+
48+
async findRecipient(handle, recipientId, userId) {
49+
return await notificationRepository.findRecipient(handle, recipientId, userId)
50+
},
51+
52+
async findAllforUser(handle, userId) {
53+
return await notificationRepository.findAllforUser(handle, userId)
54+
},
55+
56+
async getUnreadCountforUser(handle, userId) {
57+
return await notificationRepository.getUnreadCountforUser(handle, userId)
58+
},
59+
60+
async markAsRead(handle, notificationId, userId) {
61+
await notificationRepository.markAsRead(handle, notificationId, userId)
62+
},
63+
64+
async markAllAsRead(handle, userId) {
65+
await notificationRepository.markAllAsRead(handle, userId)
66+
}
67+
}
68+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "NotificationRecipient" ALTER COLUMN "read_at" DROP NOT NULL;

packages/db/prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ model DeregisterReason {
782782

783783
model NotificationRecipient {
784784
id String @id @default(uuid())
785-
readAt DateTime @map("read_at") @db.Timestamptz(3)
785+
readAt DateTime? @map("read_at") @db.Timestamptz(3)
786786
787787
notificationId String @map("notification_id")
788788
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from "./task"
1414
export * from "./user"
1515
export * from "./audit-log"
1616
export * from "./workspace-sync"
17+
export * from "./notification"

packages/types/src/notification.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { schemas } from "@dotkomonline/db/schemas"
2+
import { z } from "zod"
3+
4+
export type Notification = z.infer<typeof NotificationSchema>
5+
export const NotificationSchema = schemas.NotificationSchema
6+
export type NotificationId = Notification["id"]
7+
export type NotificationType = Notification["type"]
8+
export type NotificationPayloadType = Notification["payloadType"]
9+
10+
export const NotificationTypeSchema = schemas.NotificationTypeSchema
11+
export const NotificationPayloadTypeSchema = schemas.NotificationPayloadTypeSchema
12+
13+
export const NotificationRecipientSchema = schemas.NotificationRecipientSchema
14+
export type NotificationRecipient = z.infer<typeof NotificationRecipientSchema>
15+
16+
export type NotificationRecipientId = NotificationRecipient["id"]
17+
18+
19+
export type UserNotification = z.infer<typeof UserNotificationSchema>
20+
export const UserNotificationSchema = schemas.NotificationRecipientSchema.extend({
21+
notification: schemas.NotificationSchema,
22+
})
23+
24+
export type NotificationWrite = z.infer<typeof NotificationWriteSchema>
25+
export const NotificationWriteSchema = NotificationSchema.pick({
26+
title: true,
27+
shortDescription: true,
28+
content: true,
29+
type: true,
30+
payload: true,
31+
payloadType: true,
32+
actorGroupId: true,
33+
taskId: true,
34+
}).extend({
35+
recipientIds: z.array(z.string().uuid()).min(1),
36+
})
37+
38+

0 commit comments

Comments
 (0)