-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Messages #2781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Messages #2781
Changes from all commits
9a24c31
f32a594
ec4050a
db77984
c15077b
b4a76ba
1a55045
34eb01f
d708c10
cbe7d07
665c4ef
2e3e857
c5daa02
dae7abd
c6edd29
264487b
82f520a
cffdf98
8cb62e7
d48e725
1c1da38
2ec5eb5
c0c8670
8ccffe6
1db5bbc
1d1e528
83ba924
32d6e4f
d329e2e
452d64d
57e80bb
dc5cccb
64e4bbb
d0b1ce9
7bcf41b
0f11f4a
3aaafed
418fdf1
d5210df
e53b83d
cf66b01
306ab1c
3cfad10
065895e
04ad2cd
5ed0906
e6c11bf
5113717
0489394
687e7f4
e04059a
991da0b
b13530c
2eec587
a614c9c
b9ebffe
0978298
9775b08
296c4e8
766e6e7
8dde32b
56a173e
db41320
fd014c1
b5790fb
eaae58e
248d389
46062ef
7aefb1d
44d21fd
f2681b4
abd3f0d
8fbe1dd
e904044
8e57a36
e5fb6a7
873e9f8
f0ac1e4
4523385
90f3f02
27670a4
14f4402
4e01900
5999e7e
8ae07f5
7414c77
723cb3c
9dd8b06
812596d
be66a00
8f35c61
186486b
824879e
106330f
ceaefd8
8a35338
162fb5b
210a5e2
ca0fc12
c1355e9
c982ce0
3103a45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,166 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { handleAndReturnErrorResponse } from "@/lib/api/errors"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { verifyQstashSignature } from "@/lib/cron/verify-qstash"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { resend } from "@dub/email/resend"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { VARIANT_TO_FROM_MAP } from "@dub/email/resend/constants"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import NewMessageFromProgram from "@dub/email/templates/new-message-from-program"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { prisma } from "@dub/prisma"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NotificationEmailType } from "@dub/prisma/client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { log } from "@dub/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { subDays } from "date-fns"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { logAndRespond } from "../../utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const dynamic = "force-dynamic"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const schema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| programId: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partnerId: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastMessageId: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // POST /api/cron/messages/notify-partner | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Notify a partner about unread messages from a program | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(req: Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawBody = await req.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await verifyQstashSignature({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rawBody, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { programId, partnerId, lastMessageId } = schema.parse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.parse(rawBody), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const programEnrollment = await prisma.programEnrollment.findUniqueOrThrow({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partnerId_programId: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partnerId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| programId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "approved", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| program: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partner: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| programId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| senderPartnerId: null, // Not sent by the partner | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gt: subDays(new Date(), 3), // Sent in the last 3 days | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readInApp: null, // Unread | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readInEmail: null, // Unread | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emails: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| none: {}, // No emails sent yet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| senderUser: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| users: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| notificationPreferences: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| newMessageFromProgram: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const unreadMessages = programEnrollment.partner.messages.sort( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (unreadMessages.length === 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `No unread messages found for partner ${partnerId} in program ${programId}. Skipping...`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (unreadMessages[unreadMessages.length - 1].id !== lastMessageId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `There is a more recent unread message than ${lastMessageId}. Skipping...`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const partnerEmailsToNotify = programEnrollment.partner.users | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(({ user }) => user.email) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) as string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (partnerEmailsToNotify.length === 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `No partner emails to notify for partner ${partnerId}. Skipping...`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const program = programEnrollment.program; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data, error } = await resend.batch.send( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partnerEmailsToNotify.map((email) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subject: `${program.name} sent ${unreadMessages.length === 1 ? "a message" : `${unreadMessages.length} messages`}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from: VARIANT_TO_FROM_MAP.notifications, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to: email, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| react: NewMessageFromProgram({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| program: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: program.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logo: program.logo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slug: program.slug, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: unreadMessages.map((message) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: message.text, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: message.createdAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user: message.senderUser.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: message.senderUser.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image: message.senderUser.image, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: program.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image: program.logo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+116
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possible NPE: senderUser can be null. Guard with optional chaining before accessing name/image. messages: unreadMessages.map((message) => ({
text: message.text,
createdAt: message.createdAt,
- user: message.senderUser.name
- ? {
- name: message.senderUser.name,
- image: message.senderUser.image,
- }
- : {
- name: program.name,
- image: program.logo,
- },
+ user: message.senderUser?.name
+ ? {
+ name: message.senderUser.name,
+ image: message.senderUser.image,
+ }
+ : {
+ name: program.name,
+ image: program.logo,
+ },
})),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tags: [{ name: "type", value: "message-notification" }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Error sending message emails to partner ${partnerId}: ${error.message}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `No data received from sending message emails to partner ${partnerId}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await prisma.notificationEmail.createMany({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: unreadMessages.flatMap((message) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.data.map(({ id }) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: NotificationEmailType.Message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emailId: id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageId: message.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Emails sent for messages from program ${programId} to partner ${partnerId}.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await log({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: `Error notifying partner of new messages: ${error.message}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "alerts", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return handleAndReturnErrorResponse(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,167 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { handleAndReturnErrorResponse } from "@/lib/api/errors"; | ||||||||||||||||||||||||||||||||||||||||
| import { verifyQstashSignature } from "@/lib/cron/verify-qstash"; | ||||||||||||||||||||||||||||||||||||||||
| import { resend } from "@dub/email/resend"; | ||||||||||||||||||||||||||||||||||||||||
| import { VARIANT_TO_FROM_MAP } from "@dub/email/resend/constants"; | ||||||||||||||||||||||||||||||||||||||||
| import NewMessageFromPartner from "@dub/email/templates/new-message-from-partner"; | ||||||||||||||||||||||||||||||||||||||||
| import { prisma } from "@dub/prisma"; | ||||||||||||||||||||||||||||||||||||||||
| import { NotificationEmailType } from "@dub/prisma/client"; | ||||||||||||||||||||||||||||||||||||||||
| import { log } from "@dub/utils"; | ||||||||||||||||||||||||||||||||||||||||
| import { subDays } from "date-fns"; | ||||||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||||
| import { logAndRespond } from "../../utils"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const dynamic = "force-dynamic"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const schema = z.object({ | ||||||||||||||||||||||||||||||||||||||||
| programId: z.string(), | ||||||||||||||||||||||||||||||||||||||||
| partnerId: z.string(), | ||||||||||||||||||||||||||||||||||||||||
| lastMessageId: z.string(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // POST /api/cron/messages/notify-program | ||||||||||||||||||||||||||||||||||||||||
| // Notify a program about unread messages from a partner | ||||||||||||||||||||||||||||||||||||||||
| export async function POST(req: Request) { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| const rawBody = await req.text(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| await verifyQstashSignature({ | ||||||||||||||||||||||||||||||||||||||||
| req, | ||||||||||||||||||||||||||||||||||||||||
| rawBody, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const { programId, partnerId, lastMessageId } = schema.parse( | ||||||||||||||||||||||||||||||||||||||||
| JSON.parse(rawBody), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const programEnrollment = await prisma.programEnrollment.findUniqueOrThrow({ | ||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||
| partnerId_programId: { | ||||||||||||||||||||||||||||||||||||||||
| partnerId, | ||||||||||||||||||||||||||||||||||||||||
| programId, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| status: "approved", | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix non-unique Prisma query (use findFirstOrThrow). findUniqueOrThrow cannot take non-unique filters like status. Use findFirstOrThrow with a composite where. - const programEnrollment = await prisma.programEnrollment.findUniqueOrThrow({
- where: {
- partnerId_programId: {
- partnerId,
- programId,
- },
- status: "approved",
- },
+ const programEnrollment = await prisma.programEnrollment.findFirstOrThrow({
+ where: {
+ partnerId,
+ programId,
+ status: "approved",
+ },📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| program: { | ||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||
| workspace: { | ||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||
| users: { | ||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||
| user: true, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||
| notificationPreference: { | ||||||||||||||||||||||||||||||||||||||||
| newMessageFromPartner: true, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo in relation filter: notificationPreferences (plural). The partner-facing counterpart uses notificationPreferences; singular will fail. - notificationPreference: {
+ notificationPreferences: {
newMessageFromPartner: true,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| partner: { | ||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||
| messages: { | ||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||
| programId, | ||||||||||||||||||||||||||||||||||||||||
| senderPartnerId: { | ||||||||||||||||||||||||||||||||||||||||
| not: null, // Sent by the partner | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| createdAt: { | ||||||||||||||||||||||||||||||||||||||||
| gt: subDays(new Date(), 3), // Sent in the last 3 days | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| readInApp: null, // Unread | ||||||||||||||||||||||||||||||||||||||||
| readInEmail: null, // Unread | ||||||||||||||||||||||||||||||||||||||||
| emails: { | ||||||||||||||||||||||||||||||||||||||||
| none: {}, // No emails sent yet | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||
| senderPartner: true, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const unreadMessages = programEnrollment.partner.messages.sort( | ||||||||||||||||||||||||||||||||||||||||
| (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (unreadMessages.length === 0) | ||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||
| `No unread messages found from partner ${partnerId} in program ${programId}. Skipping...`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (unreadMessages[unreadMessages.length - 1].id !== lastMessageId) | ||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||
| `There is a more recent unread message than ${lastMessageId}. Skipping...`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const userEmailsToNotify = programEnrollment.program.workspace.users | ||||||||||||||||||||||||||||||||||||||||
| .map(({ user }) => user.email) | ||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) as string[]; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (userEmailsToNotify.length === 0) | ||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||
| `No program user emails to notify from partner ${partnerId}. Skipping...`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const { program, partner } = programEnrollment; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const { data, error } = await resend.batch.send( | ||||||||||||||||||||||||||||||||||||||||
| userEmailsToNotify.map((email) => ({ | ||||||||||||||||||||||||||||||||||||||||
| subject: `${unreadMessages.length === 1 ? "New message from" : `${unreadMessages.length} new messages from`} ${partner.name}`, | ||||||||||||||||||||||||||||||||||||||||
| from: VARIANT_TO_FROM_MAP.notifications, | ||||||||||||||||||||||||||||||||||||||||
| to: email, | ||||||||||||||||||||||||||||||||||||||||
| react: NewMessageFromPartner({ | ||||||||||||||||||||||||||||||||||||||||
| workspaceSlug: program.workspace.slug, | ||||||||||||||||||||||||||||||||||||||||
| partner: { | ||||||||||||||||||||||||||||||||||||||||
| id: partner.id, | ||||||||||||||||||||||||||||||||||||||||
| name: partner.name, | ||||||||||||||||||||||||||||||||||||||||
| image: partner.image, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| messages: unreadMessages.map((message) => ({ | ||||||||||||||||||||||||||||||||||||||||
| text: message.text, | ||||||||||||||||||||||||||||||||||||||||
| createdAt: message.createdAt, | ||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| tags: [{ name: "type", value: "message-notification" }], | ||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (error) | ||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||
| `Error sending message emails to program ${programId} users: ${error.message}`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!data) | ||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||
| `No data received from sending message emails to program ${programId} users`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| await prisma.notificationEmail.createMany({ | ||||||||||||||||||||||||||||||||||||||||
| data: unreadMessages.flatMap((message) => | ||||||||||||||||||||||||||||||||||||||||
| data.data.map(({ id }) => ({ | ||||||||||||||||||||||||||||||||||||||||
| type: NotificationEmailType.Message, | ||||||||||||||||||||||||||||||||||||||||
| emailId: id, | ||||||||||||||||||||||||||||||||||||||||
| messageId: message.id, | ||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+146
to
+154
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Make writes idempotent on retries. Enable skipDuplicates to guard against replays/partial failures. - await prisma.notificationEmail.createMany({
- data: unreadMessages.flatMap((message) =>
+ await prisma.notificationEmail.createMany({
+ skipDuplicates: true,
+ data: unreadMessages.flatMap((message) =>
data.data.map(({ id }) => ({
type: NotificationEmailType.Message,
emailId: id,
messageId: message.id,
})),
),
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return logAndRespond( | ||||||||||||||||||||||||||||||||||||||||
| `Emails sent for messages from partner ${partnerId} to program ${programId} users.`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| await log({ | ||||||||||||||||||||||||||||||||||||||||
| message: `Error notifying program users of new messages: ${error.message}`, | ||||||||||||||||||||||||||||||||||||||||
| type: "alerts", | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return handleAndReturnErrorResponse(error); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; | ||
| import { withWorkspace } from "@/lib/auth"; | ||
| import { countMessagesQuerySchema } from "@/lib/zod/schemas/messages"; | ||
| import { prisma } from "@dub/prisma"; | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| // GET /api/messages/count - count messages for a program | ||
| export const GET = withWorkspace( | ||
| async ({ workspace, searchParams }) => { | ||
| const programId = getDefaultProgramIdOrThrow(workspace); | ||
|
|
||
| const { unread } = countMessagesQuerySchema.parse(searchParams); | ||
|
|
||
| const count = await prisma.message.count({ | ||
| where: { | ||
| programId, | ||
| ...(unread !== undefined && { | ||
| // Only count messages from the partner | ||
| senderPartnerId: { | ||
| not: null, | ||
| }, | ||
| readInApp: unread | ||
| ? // Only count unread messages | ||
| null | ||
| : { | ||
| // Only count read messages | ||
| not: null, | ||
| }, | ||
| }), | ||
| }, | ||
| }); | ||
|
Comment on lines
+14
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add supporting DB indexes for this count query. This path filters on @@index([programId, senderPartnerId, readInApp])Without it, this endpoint can degrade under load as volume grows. 🤖 Prompt for AI Agents |
||
|
|
||
| return NextResponse.json(count); | ||
| }, | ||
| { | ||
| requiredPermissions: ["messages.read"], | ||
| requiredPlan: ["advanced", "enterprise"], | ||
| }, | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prisma: findUniqueOrThrow cannot filter by non-unique field "status".
Use findFirstOrThrow with a composite where instead.
📝 Committable suggestion
🤖 Prompt for AI Agents