chore: initial commit — monorepo ReadaBook (API NestJS, web PWA, Docker)

This commit is contained in:
Git Agent
2026-08-23 09:56:53 +02:00
commit 8f1140127f
79 changed files with 6456 additions and 0 deletions

View File

@ -0,0 +1,31 @@
import { Body, Controller, Get, Param, Put, UseGuards } from "@nestjs/common";
import { UpdateProgressDto, UpdateProgressSchema } from "@readabook/shared";
import { AuthGuard } from "../auth/auth.guard.js";
import { CurrentUser, CurrentUserParam } from "../auth/current-user.js";
import { ZodValidationPipe } from "../common/zod-validation.pipe.js";
import { ProgressService } from "./progress.service.js";
@Controller("progress")
@UseGuards(AuthGuard)
export class ProgressController {
constructor(private readonly progress: ProgressService) {}
@Get("continue")
continue(@CurrentUserParam() user: CurrentUser) {
return this.progress.continueReading(user.id);
}
@Get(":bookId")
get(@CurrentUserParam() user: CurrentUser, @Param("bookId") bookId: string) {
return this.progress.get(user.id, Number(bookId));
}
@Put(":bookId")
update(
@CurrentUserParam() user: CurrentUser,
@Param("bookId") bookId: string,
@Body(new ZodValidationPipe(UpdateProgressSchema)) body: UpdateProgressDto
) {
return this.progress.upsert(user.id, Number(bookId), body);
}
}

View File

@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { AuthModule } from "../auth/auth.module.js";
import { DatabaseModule } from "../database/database.module.js";
import { ProgressController } from "./progress.controller.js";
import { ProgressService } from "./progress.service.js";
@Module({
imports: [AuthModule, DatabaseModule],
controllers: [ProgressController],
providers: [ProgressService]
})
export class ProgressModule {}

View File

@ -0,0 +1,67 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { desc, eq, sql } from "drizzle-orm";
import { UpdateProgressDto } from "@readabook/shared";
import { DatabaseService } from "../database/database.service.js";
import { books, progress } from "../database/schema.js";
@Injectable()
export class ProgressService {
constructor(private readonly database: DatabaseService) {}
upsert(userId: number, bookId: number, input: UpdateProgressDto) {
const book = this.database.db.select({ id: books.id }).from(books).where(eq(books.id, bookId)).get();
if (!book) {
throw new NotFoundException("Book not found");
}
const now = this.database.now();
this.database.sqlite
.prepare(
`
INSERT INTO progress(user_id, book_id, locator, percent, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, book_id) DO UPDATE SET
locator = excluded.locator,
percent = excluded.percent,
updated_at = excluded.updated_at
`
)
.run(userId, bookId, input.locator, Math.round(input.percent), now, now);
return this.get(userId, bookId);
}
get(userId: number, bookId: number) {
const row = this.database.db
.select({
bookId: progress.bookId,
locator: progress.locator,
percent: progress.percent,
updatedAt: progress.updatedAt
})
.from(progress)
.where(sql`${progress.userId} = ${userId} AND ${progress.bookId} = ${bookId}`)
.get();
if (!row) {
throw new NotFoundException("Progress not found");
}
return row;
}
continueReading(userId: number) {
return this.database.db
.select({
book: books,
progress: {
bookId: progress.bookId,
locator: progress.locator,
percent: progress.percent,
updatedAt: progress.updatedAt
}
})
.from(progress)
.innerJoin(books, eq(progress.bookId, books.id))
.where(eq(progress.userId, userId))
.orderBy(desc(progress.updatedAt))
.limit(20)
.all();
}
}