Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@anthropic-ai/sdk": "^0.78.0",
"@fastify/cors": "^11.0.0",
"@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0",
"@fastify/websocket": "^11.2.0",
"@google/generative-ai": "^0.24.1",
"@vitals/shared": "*",
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { wsChatRoutes } from './routes/ws-chat.js';
import { uploadRoutes } from './routes/upload.js';
import { actionItemRoutes } from './routes/action-items.js';
import multipart from '@fastify/multipart';
import rateLimit from '@fastify/rate-limit';
import websocket from '@fastify/websocket';
import { databasePlugin } from './plugins/database.js';
import { registerProviders } from './services/collectors/register.js';
Expand All @@ -30,6 +31,7 @@ export async function buildApp(env: EnvConfig) {
});

await app.register(multipart);
await app.register(rateLimit, { max: 60, timeWindow: '1 minute' });
await app.register(websocket);
await app.register(databasePlugin, { env });

Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/db/migrations/009_unify_user_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Unify user_id values: REST route previously used 'default' while WebSocket used the UUID.
-- Migrate all 'default' conversations to the standard UUID so they remain accessible.
UPDATE conversations
SET user_id = '00000000-0000-0000-0000-000000000001'
WHERE user_id = 'default';
46 changes: 38 additions & 8 deletions packages/backend/src/db/queries/__tests__/conversations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,28 @@ describe('conversation queries', () => {

it('getConversation returns null when no rows returned', async () => {
const pool = makePool([]);
const result = await getConversation(pool, 'uuid-nonexistent');
const result = await getConversation(pool, 'uuid-nonexistent', 'default');
expect(result).toBeNull();
});

it('getConversation returns mapped row when found', async () => {
const pool = makePool([fakeConvRow]);
const result = await getConversation(pool, 'uuid-1');
const result = await getConversation(pool, 'uuid-1', 'default');
expect(result?.id).toBe('uuid-1');
expect(result?.title).toBe('My chat');
});

it('getConversation includes user_id in query', async () => {
const pool = makePool([fakeConvRow]);
await getConversation(pool, 'uuid-1', 'default');
const [sql, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0] as [
string,
unknown[],
];
expect(sql).toMatch(/user_id/);
expect(params).toContain('default');
});

it('createConversation inserts and returns mapped row', async () => {
const pool = makePool([fakeConvRow]);
const result = await createConversation(pool, 'default', 'My chat');
Expand Down Expand Up @@ -81,25 +92,44 @@ describe('conversation queries', () => {

it('getMessages returns mapped array', async () => {
const pool = makePool([fakeMsgRow]);
const result = await getMessages(pool, 'uuid-1');
const result = await getMessages(pool, 'uuid-1', 'default');
expect(result[0].role).toBe('user');
expect(result[0].content).toBe('Hello');
});

it('deleteConversation calls DELETE', async () => {
it('getMessages includes user_id via join', async () => {
const pool = makePool([fakeMsgRow]);
await getMessages(pool, 'uuid-1', 'default');
const [sql, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0] as [
string,
unknown[],
];
expect(sql).toMatch(/user_id/);
expect(params).toContain('default');
});

it('deleteConversation calls DELETE with user_id', async () => {
const pool = makePool([]);
await deleteConversation(pool, 'uuid-1');
expect((pool.query as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatch(/DELETE/);
await deleteConversation(pool, 'uuid-1', 'default');
const [sql, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0] as [
string,
unknown[],
];
expect(sql).toMatch(/DELETE/);
expect(sql).toMatch(/user_id/);
expect(params).toContain('default');
});

it('updateConversationTitle calls UPDATE', async () => {
it('updateConversationTitle calls UPDATE with user_id', async () => {
const pool = makePool([]);
await updateConversationTitle(pool, 'uuid-1', 'New title');
await updateConversationTitle(pool, 'uuid-1', 'New title', 'default');
const [sql, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0] as [
string,
unknown[],
];
expect(sql).toMatch(/UPDATE/);
expect(sql).toMatch(/user_id/);
expect(params).toContain('New title');
expect(params).toContain('default');
});
});
37 changes: 26 additions & 11 deletions packages/backend/src/db/queries/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ export async function createConversation(
return rowToConversation(rows[0] as Record<string, unknown>);
}

export async function getConversation(pool: pg.Pool, id: string): Promise<ConversationRow | null> {
const { rows } = await pool.query(`SELECT * FROM conversations WHERE id = $1`, [id]);
export async function getConversation(
pool: pg.Pool,
id: string,
userId: string,
): Promise<ConversationRow | null> {
const { rows } = await pool.query(`SELECT * FROM conversations WHERE id = $1 AND user_id = $2`, [
id,
userId,
]);
if (rows.length === 0) return null;
return rowToConversation(rows[0] as Record<string, unknown>);
}
Expand All @@ -74,15 +81,16 @@ export async function updateConversationTitle(
pool: pg.Pool,
id: string,
title: string,
userId: string,
): Promise<void> {
await pool.query(`UPDATE conversations SET title = $1, updated_at = NOW() WHERE id = $2`, [
title,
id,
]);
await pool.query(
`UPDATE conversations SET title = $1, updated_at = NOW() WHERE id = $2 AND user_id = $3`,
[title, id, userId],
);
}

export async function deleteConversation(pool: pg.Pool, id: string): Promise<void> {
await pool.query(`DELETE FROM conversations WHERE id = $1`, [id]);
export async function deleteConversation(pool: pg.Pool, id: string, userId: string): Promise<void> {
await pool.query(`DELETE FROM conversations WHERE id = $1 AND user_id = $2`, [id, userId]);
}

export async function addMessage(
Expand Down Expand Up @@ -111,10 +119,17 @@ export async function addMessage(
return rowToMessage(rows[0] as Record<string, unknown>);
}

export async function getMessages(pool: pg.Pool, conversationId: string): Promise<MessageRow[]> {
export async function getMessages(
pool: pg.Pool,
conversationId: string,
userId: string,
): Promise<MessageRow[]> {
const { rows } = await pool.query(
`SELECT * FROM messages WHERE conversation_id = $1 ORDER BY created_at`,
[conversationId],
`SELECT m.* FROM messages m
JOIN conversations c ON m.conversation_id = c.id
WHERE m.conversation_id = $1 AND c.user_id = $2
ORDER BY m.created_at`,
[conversationId, userId],
);
return rows.map((r) => rowToMessage(r as Record<string, unknown>));
}
50 changes: 42 additions & 8 deletions packages/backend/src/routes/__tests__/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ vi.mock('../../services/collectors/register.js', () => ({
// Mock conversation queries so tests don't hit the DB
vi.mock('../../db/queries/conversations.js', () => ({
createConversation: vi.fn().mockResolvedValue({
id: 'conv-1',
id: '00000000-0000-0000-0000-000000000001',
title: null,
userId: 'default',
createdAt: new Date(),
updatedAt: new Date(),
}),
getConversation: vi.fn().mockResolvedValue({
id: 'conv-1',
id: '00000000-0000-0000-0000-000000000001',
title: null,
userId: 'default',
createdAt: new Date(),
Expand Down Expand Up @@ -98,6 +98,17 @@ describe('POST /api/chat', () => {
await app.close();
});

it('returns 400 when conversationId is not a valid UUID', async () => {
const app = await buildApp(testEnv);
const response = await app.inject({
method: 'POST',
url: '/api/chat',
payload: { message: 'Hello', conversationId: 'not-a-uuid' },
});
expect(response.statusCode).toBe(400);
await app.close();
});

it('returns 404 when conversationId does not exist', async () => {
const { getConversation } = await import('../../db/queries/conversations.js');
(getConversation as ReturnType<typeof vi.fn>).mockResolvedValueOnce(null);
Expand All @@ -106,7 +117,10 @@ describe('POST /api/chat', () => {
const response = await app.inject({
method: 'POST',
url: '/api/chat',
payload: { message: 'Hello', conversationId: 'nonexistent' },
payload: {
message: 'Hello',
conversationId: '00000000-0000-0000-0000-000000000099',
},
});
expect(response.statusCode).toBe(404);
await app.close();
Expand All @@ -121,7 +135,7 @@ describe('POST /api/chat', () => {
});
expect(response.statusCode).toBe(200);
const body = response.json<{ conversationId: string; response: string }>();
expect(body.conversationId).toBe('conv-1');
expect(body.conversationId).toBe('00000000-0000-0000-0000-000000000001');
expect(body.response).toContain('150g');
await app.close();
});
Expand All @@ -142,14 +156,24 @@ describe('GET /api/chat/conversations', () => {
});

describe('GET /api/chat/conversations/:id', () => {
it('returns 400 for invalid UUID', async () => {
const app = await buildApp(testEnv);
const response = await app.inject({
method: 'GET',
url: '/api/chat/conversations/not-a-uuid',
});
expect(response.statusCode).toBe(400);
await app.close();
});

it('returns 404 when conversation does not exist', async () => {
const { getConversation } = await import('../../db/queries/conversations.js');
(getConversation as ReturnType<typeof vi.fn>).mockResolvedValueOnce(null);

const app = await buildApp(testEnv);
const response = await app.inject({
method: 'GET',
url: '/api/chat/conversations/nonexistent',
url: '/api/chat/conversations/00000000-0000-0000-0000-000000000099',
});
expect(response.statusCode).toBe(404);
await app.close();
Expand All @@ -159,7 +183,7 @@ describe('GET /api/chat/conversations/:id', () => {
const app = await buildApp(testEnv);
const response = await app.inject({
method: 'GET',
url: '/api/chat/conversations/conv-1',
url: '/api/chat/conversations/00000000-0000-0000-0000-000000000001',
});
expect(response.statusCode).toBe(200);
const body = response.json<{ conversation: unknown; messages: unknown[] }>();
Expand All @@ -170,14 +194,24 @@ describe('GET /api/chat/conversations/:id', () => {
});

describe('DELETE /api/chat/conversations/:id', () => {
it('returns 400 for invalid UUID', async () => {
const app = await buildApp(testEnv);
const response = await app.inject({
method: 'DELETE',
url: '/api/chat/conversations/not-a-uuid',
});
expect(response.statusCode).toBe(400);
await app.close();
});

it('returns 404 when conversation does not exist', async () => {
const { getConversation } = await import('../../db/queries/conversations.js');
(getConversation as ReturnType<typeof vi.fn>).mockResolvedValueOnce(null);

const app = await buildApp(testEnv);
const response = await app.inject({
method: 'DELETE',
url: '/api/chat/conversations/nonexistent',
url: '/api/chat/conversations/00000000-0000-0000-0000-000000000099',
});
expect(response.statusCode).toBe(404);
await app.close();
Expand All @@ -187,7 +221,7 @@ describe('DELETE /api/chat/conversations/:id', () => {
const app = await buildApp(testEnv);
const response = await app.inject({
method: 'DELETE',
url: '/api/chat/conversations/conv-1',
url: '/api/chat/conversations/00000000-0000-0000-0000-000000000001',
});
expect(response.statusCode).toBe(204);
await app.close();
Expand Down
Loading
Loading