0a9588abb7
- 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
219 lines
5.7 KiB
TypeScript
219 lines
5.7 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { Note } from './note.entity';
|
|
import { KnowledgeGroup } from '../knowledge-group/knowledge-group.entity';
|
|
import { OcrService } from '../ocr/ocr.service';
|
|
import * as fs from 'fs/promises';
|
|
import * as path from 'path';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { I18nService } from '../i18n/i18n.service';
|
|
|
|
@Injectable()
|
|
export class NoteService {
|
|
// Directory will be created dynamically per user
|
|
private getScreenshotsDir(userId: string) {
|
|
return path.join(process.cwd(), 'uploads', 'notes-screenshots', userId);
|
|
}
|
|
|
|
constructor(
|
|
@InjectRepository(Note)
|
|
private readonly noteRepository: Repository<Note>,
|
|
private readonly ocrService: OcrService,
|
|
private readonly i18nService: I18nService,
|
|
) {}
|
|
|
|
private async ensureScreenshotsDir(userId: string) {
|
|
const dir = this.getScreenshotsDir(userId);
|
|
try {
|
|
await fs.access(dir);
|
|
} catch {
|
|
await fs.mkdir(dir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
async create(
|
|
userId: string,
|
|
data: Partial<Note>,
|
|
): Promise<Note> {
|
|
// Handle empty strings for foreign keys
|
|
if (data.groupId === '') {
|
|
data.groupId = null as any;
|
|
}
|
|
if (data.categoryId === '') {
|
|
data.categoryId = null as any;
|
|
}
|
|
|
|
const note = this.noteRepository.create({
|
|
...data,
|
|
userId,
|
|
});
|
|
return this.noteRepository.save(note);
|
|
}
|
|
|
|
async findAll(
|
|
userId: string,
|
|
isAdmin: boolean,
|
|
groupId?: string,
|
|
categoryId?: string,
|
|
): Promise<Note[]> {
|
|
const query = this.noteRepository
|
|
.createQueryBuilder('note')
|
|
.leftJoinAndSelect('note.user', 'user');
|
|
|
|
if (!isAdmin) {
|
|
query.where('note.userId = :userId', { userId });
|
|
}
|
|
|
|
if (groupId) {
|
|
query.andWhere('note.groupId = :groupId', { groupId });
|
|
}
|
|
|
|
if (categoryId) {
|
|
query.andWhere('note.categoryId = :categoryId', { categoryId });
|
|
}
|
|
|
|
return query.getMany();
|
|
}
|
|
|
|
async findOne(
|
|
userId: string,
|
|
id: string,
|
|
isAdmin: boolean,
|
|
): Promise<Note> {
|
|
let note;
|
|
if (isAdmin) {
|
|
note = await this.noteRepository.findOne({
|
|
where: { id },
|
|
relations: ['user'],
|
|
});
|
|
} else {
|
|
note = await this.noteRepository.findOne({
|
|
where: { id, userId },
|
|
relations: ['user'],
|
|
});
|
|
}
|
|
|
|
if (!note) {
|
|
throw new NotFoundException(
|
|
this.i18nService.formatMessage('noteNotFound', { id }),
|
|
);
|
|
}
|
|
return note;
|
|
}
|
|
|
|
async update(
|
|
userId: string,
|
|
id: string,
|
|
data: Partial<Note>,
|
|
isAdmin: boolean,
|
|
): Promise<Note> {
|
|
const note = await this.findOne(userId, id, isAdmin);
|
|
// Remove protected fields
|
|
delete (data as any).id;
|
|
delete (data as any).userId;
|
|
delete (data as any).createdAt;
|
|
|
|
// Handle empty strings for foreign keys
|
|
if (data.groupId === '') {
|
|
data.groupId = null as any;
|
|
}
|
|
if (data.categoryId === '') {
|
|
data.categoryId = null as any;
|
|
}
|
|
|
|
Object.assign(note, data);
|
|
return this.noteRepository.save(note);
|
|
}
|
|
|
|
async createFromPDFSelection(
|
|
userId: string,
|
|
fileId: string,
|
|
screenshot: Express.Multer.File,
|
|
groupId?: string,
|
|
categoryId?: string,
|
|
pageNumber?: number,
|
|
): Promise<Note> {
|
|
// If groupId is provided, verify that the group exists
|
|
// We'll directly query the group to ensure it exists, regardless of user permissions
|
|
// Since all groups are accessible to all users anyway
|
|
if (groupId) {
|
|
const groupRepo =
|
|
this.noteRepository.manager.getRepository(KnowledgeGroup);
|
|
const group = await groupRepo.findOne({
|
|
where: { id: groupId },
|
|
});
|
|
|
|
if (!group) {
|
|
throw new NotFoundException(
|
|
this.i18nService.formatMessage('knowledgeGroupNotFound', {
|
|
id: groupId,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Optional: Add logging to help debug permission issues
|
|
console.log(`User ${userId} attempting to add note to group ${groupId}`);
|
|
}
|
|
|
|
if (categoryId === '') {
|
|
categoryId = null as any;
|
|
}
|
|
|
|
// Save screenshot to disk
|
|
await this.ensureScreenshotsDir(userId);
|
|
const filename = `${uuidv4()}-${Date.now()}.png`;
|
|
const screenshotPath = path.join(
|
|
this.getScreenshotsDir(userId),
|
|
filename,
|
|
);
|
|
await fs.writeFile(screenshotPath, screenshot.buffer);
|
|
|
|
// Extract text using OCR
|
|
let extractedText = '';
|
|
try {
|
|
extractedText = await this.ocrService.extractTextFromImage(
|
|
screenshot.buffer,
|
|
);
|
|
} catch (error) {
|
|
console.error('OCR extraction failed:', error);
|
|
// Continue without OCR text if extraction fails
|
|
}
|
|
|
|
// Create note with screenshot and extracted text
|
|
const note = this.noteRepository.create({
|
|
userId,
|
|
groupId: groupId || (null as any),
|
|
categoryId: categoryId || (null as any),
|
|
title: this.i18nService.formatMessage('pdfNoteTitle', {
|
|
date: new Date().toLocaleString(),
|
|
}),
|
|
content: extractedText || this.i18nService.getMessage('noTextExtracted'),
|
|
screenshotPath: `notes-screenshots/${userId}/${filename}`,
|
|
sourceFileId: fileId,
|
|
sourcePageNumber: pageNumber,
|
|
});
|
|
|
|
return this.noteRepository.save(note);
|
|
}
|
|
|
|
async remove(
|
|
userId: string,
|
|
id: string,
|
|
isAdmin: boolean,
|
|
): Promise<void> {
|
|
let result;
|
|
if (isAdmin) {
|
|
result = await this.noteRepository.delete({ id });
|
|
} else {
|
|
result = await this.noteRepository.delete({ id, userId });
|
|
}
|
|
|
|
if (result.affected === 0) {
|
|
throw new NotFoundException(
|
|
this.i18nService.formatMessage('noteNotFound', { id }),
|
|
);
|
|
}
|
|
}
|
|
}
|