- Fix pg-boss Job type imports (PgBoss.Job -> Job from pg-boss) - Replace deprecated teamConcurrency with localConcurrency - Add null checks for possibly undefined values (clients, import rows) - Fix tone type narrowing in profile.ts - Fix test type assertions (non-null assertions, explicit Record types) - Extract auth middleware into shared module - Fix rate limiter Map generic type
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import { authMiddleware } from '../middleware/auth';
|
|
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { notifications, clients } from '../db/schema';
|
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
|
import type { User } from '../lib/auth';
|
|
|
|
export const notificationRoutes = new Elysia({ prefix: '/notifications' })
|
|
.use(authMiddleware)
|
|
// List notifications
|
|
.get('/', async ({ query, user }: { query: { limit?: string; unreadOnly?: string }; user: User }) => {
|
|
const limit = query.limit ? parseInt(query.limit) : 50;
|
|
const unreadOnly = query.unreadOnly === 'true';
|
|
|
|
let conditions = [eq(notifications.userId, user.id)];
|
|
if (unreadOnly) {
|
|
conditions.push(eq(notifications.read, false));
|
|
}
|
|
|
|
const items = await db.select({
|
|
notification: notifications,
|
|
client: {
|
|
id: clients.id,
|
|
firstName: clients.firstName,
|
|
lastName: clients.lastName,
|
|
},
|
|
})
|
|
.from(notifications)
|
|
.leftJoin(clients, eq(notifications.clientId, clients.id))
|
|
.where(and(...conditions))
|
|
.orderBy(desc(notifications.createdAt))
|
|
.limit(limit);
|
|
|
|
// Unread count
|
|
const [unreadResult] = await db.select({
|
|
count: sql<number>`count(*)::int`,
|
|
})
|
|
.from(notifications)
|
|
.where(and(eq(notifications.userId, user.id), eq(notifications.read, false)));
|
|
|
|
return {
|
|
notifications: items.map(({ notification, client }) => ({
|
|
...notification,
|
|
client: client?.id ? client : null,
|
|
})),
|
|
unreadCount: unreadResult?.count || 0,
|
|
};
|
|
}, {
|
|
query: t.Object({
|
|
limit: t.Optional(t.String()),
|
|
unreadOnly: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Mark notification as read
|
|
.put('/:id/read', async ({ params, user }: { params: { id: string }; user: User }) => {
|
|
const [updated] = await db.update(notifications)
|
|
.set({ read: true })
|
|
.where(and(eq(notifications.id, params.id), eq(notifications.userId, user.id)))
|
|
.returning();
|
|
|
|
if (!updated) throw new Error('Notification not found');
|
|
return updated;
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
})
|
|
|
|
// Mark all as read
|
|
.post('/mark-all-read', async ({ user }: { user: User }) => {
|
|
await db.update(notifications)
|
|
.set({ read: true })
|
|
.where(and(eq(notifications.userId, user.id), eq(notifications.read, false)));
|
|
|
|
return { success: true };
|
|
})
|
|
|
|
// Delete notification
|
|
.delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
|
const [deleted] = await db.delete(notifications)
|
|
.where(and(eq(notifications.id, params.id), eq(notifications.userId, user.id)))
|
|
.returning({ id: notifications.id });
|
|
|
|
if (!deleted) throw new Error('Notification not found');
|
|
return { success: true };
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
});
|