fix(api): harden broadcast and assistant fallback errors
This commit is contained in:
@@ -63,6 +63,31 @@ async function sendNotificationEmail(
|
||||
}
|
||||
}
|
||||
|
||||
function rethrowNotificationReferenceError(error: unknown): never {
|
||||
const candidate = error as {
|
||||
code?: unknown;
|
||||
message?: unknown;
|
||||
meta?: { field_name?: unknown };
|
||||
};
|
||||
const fieldName = typeof candidate.meta?.field_name === "string"
|
||||
? candidate.meta.field_name.toLowerCase()
|
||||
: "";
|
||||
|
||||
if (
|
||||
typeof candidate.code === "string"
|
||||
&& (candidate.code === "P2003" || candidate.code === "P2025")
|
||||
&& fieldName.includes("sender")
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Sender user not found",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// ─── Zod Enums ────────────────────────────────────────────────────────────────
|
||||
|
||||
const categoryEnum = z.enum(["NOTIFICATION", "REMINDER", "TASK", "APPROVAL"]);
|
||||
@@ -590,28 +615,26 @@ export const notificationRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const senderId = ctx.dbUser.id;
|
||||
|
||||
// 1. Create broadcast record
|
||||
const broadcast = await ctx.db.notificationBroadcast.create({
|
||||
data: {
|
||||
senderId,
|
||||
title: input.title,
|
||||
...(input.body !== undefined ? { body: input.body } : {}),
|
||||
...(input.link !== undefined ? { link: input.link } : {}),
|
||||
category: input.category,
|
||||
priority: input.priority,
|
||||
channel: input.channel,
|
||||
targetType: input.targetType,
|
||||
...(input.targetValue !== undefined ? { targetValue: input.targetValue } : {}),
|
||||
...(input.scheduledAt !== undefined ? { scheduledAt: input.scheduledAt } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// 2. If scheduled in the future, just return the broadcast
|
||||
// Scheduled broadcasts can be stored immediately because fan-out is deferred.
|
||||
if (input.scheduledAt && input.scheduledAt > new Date()) {
|
||||
return broadcast;
|
||||
return ctx.db.notificationBroadcast.create({
|
||||
data: {
|
||||
senderId,
|
||||
title: input.title,
|
||||
...(input.body !== undefined ? { body: input.body } : {}),
|
||||
...(input.link !== undefined ? { link: input.link } : {}),
|
||||
category: input.category,
|
||||
priority: input.priority,
|
||||
channel: input.channel,
|
||||
targetType: input.targetType,
|
||||
...(input.targetValue !== undefined ? { targetValue: input.targetValue } : {}),
|
||||
...(input.scheduledAt !== undefined ? { scheduledAt: input.scheduledAt } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Resolve recipients
|
||||
// Resolve recipients before persisting immediate broadcasts so empty targets
|
||||
// do not leave orphaned broadcast rows behind.
|
||||
const recipientIds = await resolveRecipients(
|
||||
input.targetType,
|
||||
input.targetValue,
|
||||
@@ -626,6 +649,26 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
let broadcast;
|
||||
try {
|
||||
broadcast = await ctx.db.notificationBroadcast.create({
|
||||
data: {
|
||||
senderId,
|
||||
title: input.title,
|
||||
...(input.body !== undefined ? { body: input.body } : {}),
|
||||
...(input.link !== undefined ? { link: input.link } : {}),
|
||||
category: input.category,
|
||||
priority: input.priority,
|
||||
channel: input.channel,
|
||||
targetType: input.targetType,
|
||||
...(input.targetValue !== undefined ? { targetValue: input.targetValue } : {}),
|
||||
...(input.scheduledAt !== undefined ? { scheduledAt: input.scheduledAt } : {}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowNotificationReferenceError(error);
|
||||
}
|
||||
|
||||
// 4. Create individual notifications for each recipient
|
||||
const isTask = input.category === "TASK" || input.category === "APPROVAL";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user