diff --git a/@applications/api/src/modules/session/session.controller.ts b/@applications/api/src/modules/session/session.controller.ts index fbb375e..1733a47 100644 --- a/@applications/api/src/modules/session/session.controller.ts +++ b/@applications/api/src/modules/session/session.controller.ts @@ -7,14 +7,31 @@ import { HttpStatus, Param, Post, + Query, } from '@nestjs/common'; import { SessionService } from './session.service'; -import { CreateSessionDto, CreateSessionResponseDto, SessionMessageDto } from './dto/session.dto'; +import { + CreateSessionDto, + CreateSessionResponseDto, + SessionListItemDto, + SessionMessageDto, +} from './dto/session.dto'; @Controller('session') export class SessionController { constructor(private readonly sessionService: SessionService) {} + @Get() + async listSessions( + @Query('limit') limit?: string, + @Query('offset') offset?: string, + ): Promise { + return this.sessionService.listSessions({ + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + }); + } + @Post() async createSession( @Body() dto: CreateSessionDto, diff --git a/@applications/api/src/modules/session/session.service.ts b/@applications/api/src/modules/session/session.service.ts index 4a7aedf..f0887bc 100644 --- a/@applications/api/src/modules/session/session.service.ts +++ b/@applications/api/src/modules/session/session.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ConversationSessionEntity } from './entities/conversation-session.entity'; import { ConversationMessageEntity } from './entities/conversation-message.entity'; -import type { SessionMessageDto } from './dto/session.dto'; +import type { SessionMessageDto, SessionListItemDto } from './dto/session.dto'; const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours @@ -92,4 +92,58 @@ export class SessionService { await this.touchSession(options.sessionId); return this.messageRepo.save(message); } + + async listSessions(options: { + userId?: string | null; + limit?: number; + offset?: number; + } = {}): Promise { + const { userId, limit = 50, offset = 0 } = options; + + const qb = this.sessionRepo + .createQueryBuilder('s') + .orderBy('s.last_activity_at', 'DESC') + .limit(limit) + .offset(offset); + + if (userId !== undefined) { + qb.where('s.user_id = :userId', { userId }); + } + + const sessions = await qb.getMany(); + + if (sessions.length === 0) return []; + + // Fetch message counts + last user message preview in one query per session + const sessionIds = sessions.map((s) => s.id); + + const counts: { sessionId: string; count: string }[] = await this.messageRepo + .createQueryBuilder('m') + .select('m.session_id', 'sessionId') + .addSelect('COUNT(*)', 'count') + .where('m.session_id IN (:...sessionIds)', { sessionIds }) + .groupBy('m.session_id') + .getRawMany(); + + const previews: { sessionId: string; content: string }[] = await this.messageRepo + .createQueryBuilder('m') + .select('DISTINCT ON (m.session_id) m.session_id', 'sessionId') + .addSelect('m.content', 'content') + .where('m.session_id IN (:...sessionIds)', { sessionIds }) + .andWhere("m.role = 'user'") + .orderBy('m.session_id') + .addOrderBy('m.created_at', 'DESC') + .getRawMany(); + + const countMap = new Map(counts.map((c) => [c.sessionId, Number(c.count)])); + const previewMap = new Map(previews.map((p) => [p.sessionId, p.content])); + + return sessions.map((s) => ({ + session_id: s.id, + created_at: s.createdAt.toISOString(), + last_activity_at: s.lastActivityAt.toISOString(), + message_count: countMap.get(s.id) ?? 0, + preview: previewMap.get(s.id) ?? null, + })); + } }