- 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
144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
import { authMiddleware } from '../middleware/auth';
|
|
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { interactions, clients } from '../db/schema';
|
|
import { eq, and, desc } from 'drizzle-orm';
|
|
import type { User } from '../lib/auth';
|
|
|
|
export const interactionRoutes = new Elysia()
|
|
.use(authMiddleware)
|
|
// List interactions for a client
|
|
.get('/clients/:clientId/interactions', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
|
// Verify client belongs to user
|
|
const [client] = await db.select()
|
|
.from(clients)
|
|
.where(and(eq(clients.id, params.clientId), eq(clients.userId, user.id)))
|
|
.limit(1);
|
|
|
|
if (!client) throw new Error('Client not found');
|
|
|
|
const items = await db.select()
|
|
.from(interactions)
|
|
.where(and(eq(interactions.clientId, params.clientId), eq(interactions.userId, user.id)))
|
|
.orderBy(desc(interactions.contactedAt));
|
|
|
|
return items;
|
|
}, {
|
|
params: t.Object({ clientId: t.String({ format: 'uuid' }) }),
|
|
})
|
|
|
|
// Create interaction
|
|
.post('/clients/:clientId/interactions', async ({ params, body, user }: {
|
|
params: { clientId: string };
|
|
body: { type: string; title: string; description?: string; duration?: number; contactedAt: string };
|
|
user: User;
|
|
}) => {
|
|
// Verify client belongs to user
|
|
const [client] = await db.select()
|
|
.from(clients)
|
|
.where(and(eq(clients.id, params.clientId), eq(clients.userId, user.id)))
|
|
.limit(1);
|
|
|
|
if (!client) throw new Error('Client not found');
|
|
|
|
const [interaction] = await db.insert(interactions)
|
|
.values({
|
|
userId: user.id,
|
|
clientId: params.clientId,
|
|
type: body.type,
|
|
title: body.title,
|
|
description: body.description,
|
|
duration: body.duration,
|
|
contactedAt: new Date(body.contactedAt),
|
|
})
|
|
.returning();
|
|
|
|
// Auto-update lastContactedAt on the client
|
|
const contactDate = new Date(body.contactedAt);
|
|
if (!client.lastContactedAt || contactDate > client.lastContactedAt) {
|
|
await db.update(clients)
|
|
.set({ lastContactedAt: contactDate, updatedAt: new Date() })
|
|
.where(eq(clients.id, params.clientId));
|
|
}
|
|
|
|
return interaction;
|
|
}, {
|
|
params: t.Object({ clientId: t.String({ format: 'uuid' }) }),
|
|
body: t.Object({
|
|
type: t.String({ minLength: 1 }),
|
|
title: t.String({ minLength: 1 }),
|
|
description: t.Optional(t.String()),
|
|
duration: t.Optional(t.Number({ minimum: 0 })),
|
|
contactedAt: t.String(),
|
|
}),
|
|
})
|
|
|
|
// Update interaction
|
|
.put('/interactions/:id', async ({ params, body, user }: {
|
|
params: { id: string };
|
|
body: { type?: string; title?: string; description?: string; duration?: number; contactedAt?: string };
|
|
user: User;
|
|
}) => {
|
|
const updateData: Record<string, unknown> = {};
|
|
if (body.type !== undefined) updateData.type = body.type;
|
|
if (body.title !== undefined) updateData.title = body.title;
|
|
if (body.description !== undefined) updateData.description = body.description;
|
|
if (body.duration !== undefined) updateData.duration = body.duration;
|
|
if (body.contactedAt !== undefined) updateData.contactedAt = new Date(body.contactedAt);
|
|
|
|
const [updated] = await db.update(interactions)
|
|
.set(updateData)
|
|
.where(and(eq(interactions.id, params.id), eq(interactions.userId, user.id)))
|
|
.returning();
|
|
|
|
if (!updated) throw new Error('Interaction not found');
|
|
return updated;
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
body: t.Object({
|
|
type: t.Optional(t.String()),
|
|
title: t.Optional(t.String()),
|
|
description: t.Optional(t.String()),
|
|
duration: t.Optional(t.Number({ minimum: 0 })),
|
|
contactedAt: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Delete interaction
|
|
.delete('/interactions/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
|
const [deleted] = await db.delete(interactions)
|
|
.where(and(eq(interactions.id, params.id), eq(interactions.userId, user.id)))
|
|
.returning({ id: interactions.id });
|
|
|
|
if (!deleted) throw new Error('Interaction not found');
|
|
return { success: true, id: deleted.id };
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
})
|
|
|
|
// Get all recent interactions across all clients (for dashboard)
|
|
.get('/interactions/recent', async ({ query, user }: { query: { limit?: string }; user: User }) => {
|
|
const limit = query.limit ? parseInt(query.limit) : 10;
|
|
|
|
const items = await db.select({
|
|
interaction: interactions,
|
|
client: {
|
|
id: clients.id,
|
|
firstName: clients.firstName,
|
|
lastName: clients.lastName,
|
|
},
|
|
})
|
|
.from(interactions)
|
|
.innerJoin(clients, eq(interactions.clientId, clients.id))
|
|
.where(eq(interactions.userId, user.id))
|
|
.orderBy(desc(interactions.contactedAt))
|
|
.limit(limit);
|
|
|
|
return items.map(({ interaction, client }) => ({
|
|
...interaction,
|
|
client,
|
|
}));
|
|
}, {
|
|
query: t.Object({ limit: t.Optional(t.String()) }),
|
|
});
|