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,218 @@
|
||||
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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user