feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { SearchHistory } from './search-history.entity';
|
||||
import { Tenant } from '../tenant/tenant.entity';
|
||||
|
||||
@Entity('chat_messages')
|
||||
export class ChatMessage {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'search_history_id' })
|
||||
searchHistoryId: string;
|
||||
|
||||
@Column({ name: 'tenant_id', nullable: true, type: 'text' })
|
||||
tenantId: string;
|
||||
|
||||
@Column()
|
||||
role: 'user' | 'assistant';
|
||||
|
||||
@Column('text')
|
||||
content: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
sources?: string; // JSON string
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => SearchHistory, (history) => history.messages, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'search_history_id' })
|
||||
searchHistory: SearchHistory;
|
||||
|
||||
@ManyToOne(() => Tenant)
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant: Tenant;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { CombinedAuthGuard } from '../auth/combined-auth.guard';
|
||||
import { SearchHistoryService } from './search-history.service';
|
||||
import { I18nService } from '../i18n/i18n.service';
|
||||
|
||||
@Controller('search-history')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
export class SearchHistoryController {
|
||||
constructor(
|
||||
private readonly searchHistoryService: SearchHistoryService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async findAll(
|
||||
@Request() req,
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '20',
|
||||
) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const limitNum = parseInt(limit, 10) || 20;
|
||||
|
||||
return await this.searchHistoryService.findAll(
|
||||
req.user.id,
|
||||
req.user.tenantId,
|
||||
pageNum,
|
||||
limitNum,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string, @Request() req) {
|
||||
return await this.searchHistoryService.findOne(
|
||||
id,
|
||||
req.user.id,
|
||||
req.user.tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() body: { title: string; selectedGroups?: string[] },
|
||||
@Request() req,
|
||||
) {
|
||||
const history = await this.searchHistoryService.create(
|
||||
req.user.id,
|
||||
req.user.tenantId,
|
||||
body.title,
|
||||
body.selectedGroups,
|
||||
);
|
||||
return { id: history.id };
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id') id: string, @Request() req) {
|
||||
await this.searchHistoryService.remove(id, req.user.id, req.user.tenantId);
|
||||
return { message: this.i18nService.getMessage('searchHistoryDeleted') };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { ChatMessage } from './chat-message.entity';
|
||||
|
||||
@Entity('search_history')
|
||||
export class SearchHistory {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId: string;
|
||||
|
||||
@Column({ name: 'tenant_id', nullable: true })
|
||||
tenantId: string;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column({ name: 'selected_groups', nullable: true })
|
||||
selectedGroups?: string; // JSON string
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => ChatMessage, (message) => message.searchHistory)
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SearchHistory } from './search-history.entity';
|
||||
import { ChatMessage } from './chat-message.entity';
|
||||
import { SearchHistoryService } from './search-history.service';
|
||||
import { SearchHistoryController } from './search-history.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([SearchHistory, ChatMessage])],
|
||||
controllers: [SearchHistoryController],
|
||||
providers: [SearchHistoryService],
|
||||
exports: [SearchHistoryService],
|
||||
})
|
||||
export class SearchHistoryModule {}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { I18nService } from '../i18n/i18n.service';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SearchHistory } from './search-history.entity';
|
||||
import { ChatMessage } from './chat-message.entity';
|
||||
|
||||
export interface SearchHistoryItem {
|
||||
id: string;
|
||||
title: string;
|
||||
selectedGroups: string[] | null;
|
||||
messageCount: number;
|
||||
lastMessageAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SearchHistoryDetail {
|
||||
id: string;
|
||||
title: string;
|
||||
selectedGroups: string[] | null;
|
||||
messages: Array<{
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
sources?: any[];
|
||||
createdAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PaginatedSearchHistory {
|
||||
histories: SearchHistoryItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SearchHistoryService {
|
||||
constructor(
|
||||
@InjectRepository(SearchHistory)
|
||||
private searchHistoryRepository: Repository<SearchHistory>,
|
||||
@InjectRepository(ChatMessage)
|
||||
private chatMessageRepository: Repository<ChatMessage>,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async findAll(
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
): Promise<PaginatedSearchHistory> {
|
||||
const [histories, total] = await this.searchHistoryRepository.findAndCount({
|
||||
where: { userId, tenantId },
|
||||
order: { updatedAt: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
const items: SearchHistoryItem[] = await Promise.all(
|
||||
histories.map(async (history) => {
|
||||
const messageCount = await this.chatMessageRepository.count({
|
||||
where: { searchHistoryId: history.id },
|
||||
});
|
||||
|
||||
const lastMessage = await this.chatMessageRepository.findOne({
|
||||
where: { searchHistoryId: history.id },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
id: history.id,
|
||||
title: history.title,
|
||||
selectedGroups: history.selectedGroups
|
||||
? JSON.parse(history.selectedGroups)
|
||||
: null,
|
||||
messageCount,
|
||||
lastMessageAt:
|
||||
lastMessage?.createdAt.toISOString() ||
|
||||
history.createdAt.toISOString(),
|
||||
createdAt: history.createdAt.toISOString(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
histories: items,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(
|
||||
id: string,
|
||||
userId: string,
|
||||
tenantId?: string,
|
||||
): Promise<SearchHistoryDetail> {
|
||||
const whereClause: any = { id, userId };
|
||||
if (tenantId) {
|
||||
whereClause.tenantId = tenantId;
|
||||
}
|
||||
const history = await this.searchHistoryRepository.findOne({
|
||||
where: whereClause,
|
||||
relations: ['messages'],
|
||||
order: { messages: { createdAt: 'ASC' } },
|
||||
});
|
||||
|
||||
if (!history) {
|
||||
throw new NotFoundException(
|
||||
this.i18nService.getMessage('conversationHistoryNotFound'),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: history.id,
|
||||
title: history.title,
|
||||
selectedGroups: history.selectedGroups
|
||||
? JSON.parse(history.selectedGroups)
|
||||
: null,
|
||||
messages: history.messages.map((message) => ({
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
sources: message.sources ? JSON.parse(message.sources) : undefined,
|
||||
createdAt: message.createdAt.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async create(
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
title: string,
|
||||
selectedGroups?: string[],
|
||||
): Promise<SearchHistory> {
|
||||
const history = this.searchHistoryRepository.create({
|
||||
userId,
|
||||
tenantId,
|
||||
title: title.length > 50 ? title.substring(0, 50) + '...' : title,
|
||||
selectedGroups: selectedGroups
|
||||
? JSON.stringify(selectedGroups)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return await this.searchHistoryRepository.save(history);
|
||||
}
|
||||
|
||||
async addMessage(
|
||||
historyId: string,
|
||||
role: 'user' | 'assistant',
|
||||
content: string,
|
||||
sources?: any[],
|
||||
): Promise<ChatMessage> {
|
||||
const message = this.chatMessageRepository.create({
|
||||
searchHistoryId: historyId,
|
||||
role,
|
||||
content,
|
||||
sources: sources ? JSON.stringify(sources) : undefined,
|
||||
});
|
||||
|
||||
const savedMessage = await this.chatMessageRepository.save(message);
|
||||
|
||||
// Update history record update time
|
||||
await this.searchHistoryRepository.update(historyId, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
return savedMessage;
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string, tenantId: string): Promise<void> {
|
||||
const history = await this.searchHistoryRepository.findOne({
|
||||
where: { id, userId, tenantId },
|
||||
});
|
||||
|
||||
if (!history) {
|
||||
throw new NotFoundException(
|
||||
this.i18nService.getMessage('conversationHistoryNotFound'),
|
||||
);
|
||||
}
|
||||
|
||||
await this.searchHistoryRepository.remove(history);
|
||||
}
|
||||
|
||||
async updateTitle(
|
||||
id: string,
|
||||
title: string,
|
||||
tenantId?: string,
|
||||
): Promise<void> {
|
||||
const whereClause: any = { id };
|
||||
if (tenantId) {
|
||||
whereClause.tenantId = tenantId;
|
||||
}
|
||||
await this.searchHistoryRepository.update(whereClause, { title });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user