Files
ReadaBook/apps/api/src/books/books.service.ts

98 lines
2.9 KiB
TypeScript

import { Injectable, NotFoundException } from "@nestjs/common";
import { createReadStream, existsSync } from "node:fs";
import { and, eq, sql } from "drizzle-orm";
import { BookQueryDto } from "@readabook/shared";
import { DatabaseService } from "../database/database.service.js";
import { books } from "../database/schema.js";
@Injectable()
export class BooksService {
constructor(private readonly database: DatabaseService) {}
list(query: BookQueryDto) {
const filters = [];
if (query.format) filters.push(eq(books.format, query.format));
if (query.libraryId) filters.push(eq(books.libraryId, query.libraryId));
if (query.q) {
return this.search(query.q, query.limit, query.offset);
}
return this.database.db
.select()
.from(books)
.where(filters.length ? and(...filters) : undefined)
.orderBy(books.title)
.limit(query.limit)
.offset(query.offset)
.all();
}
search(q: string, limit = 50, offset = 0) {
const rows = this.database.sqlite
.prepare(
`
SELECT books.*
FROM book_fts
JOIN books ON books.id = book_fts.rowid
WHERE book_fts MATCH ?
ORDER BY bm25(book_fts)
LIMIT ? OFFSET ?
`
)
.all(`${q.replace(/"/g, '""')}*`, limit, offset);
return (rows as Array<Record<string, unknown>>).map(mapBookRow);
}
get(id: number) {
const book = this.database.db.select().from(books).where(eq(books.id, id)).get();
if (!book) {
throw new NotFoundException("Book not found");
}
return book;
}
streamFile(id: number) {
const book = this.get(id);
if (!existsSync(book.filePath)) {
throw new NotFoundException("Book file not found on disk");
}
return { book, stream: createReadStream(book.filePath) };
}
streamCover(id: number) {
const book = this.get(id);
if (!book.coverPath || !existsSync(book.coverPath)) {
throw new NotFoundException("Cover not found");
}
return { book, stream: createReadStream(book.coverPath), coverPath: book.coverPath };
}
count() {
return this.database.db.select({ count: sql<number>`count(*)` }).from(books).get()?.count ?? 0;
}
}
function mapBookRow(row: Record<string, unknown>) {
return {
id: Number(row.id),
libraryId: Number(row.library_id),
title: String(row.title),
author: nullable(row.author),
description: nullable(row.description),
isbn: nullable(row.isbn),
language: nullable(row.language),
publisher: nullable(row.publisher),
publishedDate: nullable(row.published_date),
format: row.format,
filePath: String(row.file_path),
coverPath: nullable(row.cover_path),
fileSize: Number(row.file_size),
fileMtime: String(row.file_mtime),
createdAt: String(row.created_at),
updatedAt: String(row.updated_at)
};
}
function nullable(value: unknown): string | null {
return value === null || value === undefined ? null : String(value);
}