import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common'; import { I18nService } from '../i18n/i18n.service'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { KnowledgeGroup } from './knowledge-group.entity'; import { KnowledgeBase } from '../knowledge-base/knowledge-base.entity'; import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service'; export interface CreateGroupDto { name: string; description?: string; color?: string; } export interface UpdateGroupDto { name?: string; description?: string; color?: string; } export interface GroupWithFileCount { id: string; name: string; description?: string; color: string; fileCount: number; createdAt: Date; } @Injectable() export class KnowledgeGroupService { constructor( @InjectRepository(KnowledgeGroup) private groupRepository: Repository, @InjectRepository(KnowledgeBase) private knowledgeBaseRepository: Repository, @Inject(forwardRef(() => KnowledgeBaseService)) private knowledgeBaseService: KnowledgeBaseService, private i18nService: I18nService, ) { } async findAll(userId: string, tenantId: string): Promise { // Return all groups for the tenant const groups = await this.groupRepository .createQueryBuilder('group') .leftJoin('group.knowledgeBases', 'kb') .where('group.tenantId = :tenantId', { tenantId }) .addSelect('COUNT(kb.id)', 'fileCount') .groupBy('group.id') .orderBy('group.createdAt', 'DESC') .getRawAndEntities(); return groups.entities.map((group, index) => ({ id: group.id, name: group.name, description: group.description, color: group.color, fileCount: parseInt(groups.raw[index].fileCount) || 0, createdAt: group.createdAt, })); } async findOne(id: string, userId: string, tenantId: string): Promise { // Restrict group to tenant const group = await this.groupRepository.findOne({ where: { id, tenantId }, relations: ['knowledgeBases'], }); if (!group) { throw new NotFoundException(this.i18nService.getMessage('groupNotFound')); } return group; } async create(userId: string, tenantId: string, createGroupDto: CreateGroupDto): Promise { const group = this.groupRepository.create({ ...createGroupDto, tenantId, }); return await this.groupRepository.save(group); } async update(id: string, userId: string, tenantId: string, updateGroupDto: UpdateGroupDto): Promise { // Update group within the tenant const group = await this.groupRepository.findOne({ where: { id, tenantId }, }); if (!group) { throw new NotFoundException(this.i18nService.getMessage('groupNotFound')); } Object.assign(group, updateGroupDto); return await this.groupRepository.save(group); } async remove(id: string, userId: string, tenantId: string): Promise { // Remove group within the tenant const group = await this.groupRepository.findOne({ where: { id, tenantId }, }); if (!group) { throw new NotFoundException(this.i18nService.getMessage('groupNotFound')); } // Find all files associated with this group (without user restriction) const files = await this.knowledgeBaseRepository .createQueryBuilder('kb') .innerJoin('kb.groups', 'group') .where('group.id = :groupId', { groupId: id }) .select('kb.id') .getMany(); // Delete each file for (const file of files) { try { // We need to get the file's owner to delete it properly const fullFile = await this.knowledgeBaseRepository.findOne({ where: { id: file.id }, select: ['id', 'userId', 'tenantId'] // Get the owner of the file }); if (fullFile) { await this.knowledgeBaseService.deleteFile(fullFile.id, fullFile.userId, fullFile.tenantId as string); } } catch (error) { console.error(`Failed to delete file ${file.id} when deleting group ${id}`, error); } } // Delete notes in this group - call the findAll method with groupId parameter only // We'll fetch notes for the group without userId restriction // For this, we'll call the note service differently // Actually, we need to think about this carefully // Notes belong to users, so we can't remove notes from other users' groups // We'll just remove the group from the association // Or fetch notes by groupId only (need to modify note service) // Since note service is user-restricted, let's only handle file removal // and leave note management to users individually // Delete the group itself await this.groupRepository.remove(group); } async getGroupFiles(groupId: string, userId: string, tenantId: string): Promise { const group = await this.groupRepository.findOne({ where: { id: groupId, tenantId }, relations: ['knowledgeBases'], }); if (!group) { throw new NotFoundException(this.i18nService.getMessage('groupNotFound')); } return group.knowledgeBases; } async addFilesToGroup(fileId: string, groupIds: string[], userId: string, tenantId: string): Promise { const file = await this.knowledgeBaseRepository.findOne({ where: { id: fileId, userId, tenantId }, relations: ['groups'], }); if (!file) { throw new NotFoundException(this.i18nService.getMessage('fileNotFound')); } // Load all groups by ID without user restriction const groups = await this.groupRepository.findByIds(groupIds); const validGroups = groups.filter(g => g.tenantId === tenantId); if (validGroups.length !== groupIds.length) { throw new NotFoundException(this.i18nService.getMessage('someGroupsNotFound')); } file.groups = validGroups; await this.knowledgeBaseRepository.save(file); } async removeFileFromGroup(fileId: string, groupId: string, userId: string, tenantId: string): Promise { const file = await this.knowledgeBaseRepository.findOne({ where: { id: fileId, userId, tenantId }, relations: ['groups'], }); if (!file) { throw new NotFoundException(this.i18nService.getMessage('fileNotFound')); } file.groups = file.groups.filter(group => group.id !== groupId); await this.knowledgeBaseRepository.save(file); } async getFileIdsByGroups(groupIds: string[], userId: string, tenantId: string): Promise { if (!groupIds || groupIds.length === 0) { return []; } const result = await this.knowledgeBaseRepository .createQueryBuilder('kb') .innerJoin('kb.groups', 'group') .where('group.id IN (:...groupIds)', { groupIds }) .andWhere('kb.tenantId = :tenantId', { tenantId }) .select('DISTINCT kb.id', 'id') .getRawMany(); return result.map(row => row.id); } }