aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server
diff options
context:
space:
mode:
authornsfisis <54318333+nsfisis@users.noreply.github.com>2026-01-12 18:09:14 +0900
committerGitHub <noreply@github.com>2026-01-12 18:09:14 +0900
commit188c49e6ae0dfa0af052a001bc40c26d448b1583 (patch)
treef0cebd927c2292a40f123a694c6aa561768ad7a3 /src/server
parentf8e4be9b36a16969ac53bd9ce12ce8064be10196 (diff)
parent1732c543f4bc98ec6a4c7ea13c3e4762933421d8 (diff)
downloadkioku-188c49e6ae0dfa0af052a001bc40c26d448b1583.tar.gz
kioku-188c49e6ae0dfa0af052a001bc40c26d448b1583.tar.zst
kioku-188c49e6ae0dfa0af052a001bc40c26d448b1583.zip
Merge pull request #9 from nsfisis/claude/show-daily-card-count-6dJ6tHEADmain
Display daily card count on deck list page
Diffstat (limited to 'src/server')
-rw-r--r--src/server/repositories/card.test.ts1
-rw-r--r--src/server/repositories/card.ts14
-rw-r--r--src/server/repositories/types.ts1
-rw-r--r--src/server/routes/cards.test.ts1
-rw-r--r--src/server/routes/decks.test.ts59
-rw-r--r--src/server/routes/decks.ts20
-rw-r--r--src/server/routes/study.test.ts1
7 files changed, 88 insertions, 9 deletions
diff --git a/src/server/repositories/card.test.ts b/src/server/repositories/card.test.ts
index 0a46a76..b492fd7 100644
--- a/src/server/repositories/card.test.ts
+++ b/src/server/repositories/card.test.ts
@@ -111,6 +111,7 @@ function createMockCardRepo(): CardRepository {
softDelete: vi.fn(),
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
+ countDueCards: vi.fn(),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),
diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts
index 04425a2..ac03bc6 100644
--- a/src/server/repositories/card.ts
+++ b/src/server/repositories/card.ts
@@ -204,6 +204,20 @@ export const cardRepository: CardRepository = {
return result;
},
+ async countDueCards(deckId: string, now: Date): Promise<number> {
+ const result = await db
+ .select({ count: sql<number>`count(*)::int` })
+ .from(cards)
+ .where(
+ and(
+ eq(cards.deckId, deckId),
+ isNull(cards.deletedAt),
+ lte(cards.due, now),
+ ),
+ );
+ return result[0]?.count ?? 0;
+ },
+
async findDueCardsWithNoteData(
deckId: string,
now: Date,
diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts
index 4768d49..cb3a287 100644
--- a/src/server/repositories/types.ts
+++ b/src/server/repositories/types.ts
@@ -147,6 +147,7 @@ export interface CardRepository {
softDelete(id: string, deckId: string): Promise<boolean>;
softDeleteByNoteId(noteId: string): Promise<boolean>;
findDueCards(deckId: string, now: Date, limit: number): Promise<Card[]>;
+ countDueCards(deckId: string, now: Date): Promise<number>;
findDueCardsWithNoteData(
deckId: string,
now: Date,
diff --git a/src/server/routes/cards.test.ts b/src/server/routes/cards.test.ts
index 66ba601..e5fb0d4 100644
--- a/src/server/routes/cards.test.ts
+++ b/src/server/routes/cards.test.ts
@@ -25,6 +25,7 @@ function createMockCardRepo(): CardRepository {
softDelete: vi.fn(),
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
+ countDueCards: vi.fn(),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),
diff --git a/src/server/routes/decks.test.ts b/src/server/routes/decks.test.ts
index 8f5be9d..55aca2d 100644
--- a/src/server/routes/decks.test.ts
+++ b/src/server/routes/decks.test.ts
@@ -2,7 +2,11 @@ import { Hono } from "hono";
import { sign } from "hono/jwt";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware/index.js";
-import type { Deck, DeckRepository } from "../repositories/index.js";
+import type {
+ CardRepository,
+ Deck,
+ DeckRepository,
+} from "../repositories/index.js";
import { createDecksRouter } from "./decks.js";
function createMockDeckRepo(): DeckRepository {
@@ -15,6 +19,24 @@ function createMockDeckRepo(): DeckRepository {
};
}
+function createMockCardRepo(): CardRepository {
+ return {
+ findByDeckId: vi.fn(),
+ findById: vi.fn(),
+ findByIdWithNoteData: vi.fn(),
+ findByNoteId: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ softDelete: vi.fn(),
+ softDeleteByNoteId: vi.fn(),
+ findDueCards: vi.fn(),
+ countDueCards: vi.fn().mockResolvedValue(0),
+ findDueCardsWithNoteData: vi.fn(),
+ findDueCardsForStudy: vi.fn(),
+ updateFSRSFields: vi.fn(),
+ };
+}
+
const JWT_SECRET = process.env.JWT_SECRET || "test-secret";
async function createTestToken(userId: string): Promise<string> {
@@ -57,12 +79,17 @@ interface DeckResponse {
describe("GET /api/decks", () => {
let app: Hono;
let mockDeckRepo: ReturnType<typeof createMockDeckRepo>;
+ let mockCardRepo: ReturnType<typeof createMockCardRepo>;
let authToken: string;
beforeEach(async () => {
vi.clearAllMocks();
mockDeckRepo = createMockDeckRepo();
- const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo });
+ mockCardRepo = createMockCardRepo();
+ const decksRouter = createDecksRouter({
+ deckRepo: mockDeckRepo,
+ cardRepo: mockCardRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/decks", decksRouter);
@@ -112,12 +139,17 @@ describe("GET /api/decks", () => {
describe("POST /api/decks", () => {
let app: Hono;
let mockDeckRepo: ReturnType<typeof createMockDeckRepo>;
+ let mockCardRepo: ReturnType<typeof createMockCardRepo>;
let authToken: string;
beforeEach(async () => {
vi.clearAllMocks();
mockDeckRepo = createMockDeckRepo();
- const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo });
+ mockCardRepo = createMockCardRepo();
+ const decksRouter = createDecksRouter({
+ deckRepo: mockDeckRepo,
+ cardRepo: mockCardRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/decks", decksRouter);
@@ -220,12 +252,17 @@ describe("POST /api/decks", () => {
describe("GET /api/decks/:id", () => {
let app: Hono;
let mockDeckRepo: ReturnType<typeof createMockDeckRepo>;
+ let mockCardRepo: ReturnType<typeof createMockCardRepo>;
let authToken: string;
beforeEach(async () => {
vi.clearAllMocks();
mockDeckRepo = createMockDeckRepo();
- const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo });
+ mockCardRepo = createMockCardRepo();
+ const decksRouter = createDecksRouter({
+ deckRepo: mockDeckRepo,
+ cardRepo: mockCardRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/decks", decksRouter);
@@ -285,12 +322,17 @@ describe("GET /api/decks/:id", () => {
describe("PUT /api/decks/:id", () => {
let app: Hono;
let mockDeckRepo: ReturnType<typeof createMockDeckRepo>;
+ let mockCardRepo: ReturnType<typeof createMockCardRepo>;
let authToken: string;
beforeEach(async () => {
vi.clearAllMocks();
mockDeckRepo = createMockDeckRepo();
- const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo });
+ mockCardRepo = createMockCardRepo();
+ const decksRouter = createDecksRouter({
+ deckRepo: mockDeckRepo,
+ cardRepo: mockCardRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/decks", decksRouter);
@@ -410,12 +452,17 @@ describe("PUT /api/decks/:id", () => {
describe("DELETE /api/decks/:id", () => {
let app: Hono;
let mockDeckRepo: ReturnType<typeof createMockDeckRepo>;
+ let mockCardRepo: ReturnType<typeof createMockCardRepo>;
let authToken: string;
beforeEach(async () => {
vi.clearAllMocks();
mockDeckRepo = createMockDeckRepo();
- const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo });
+ mockCardRepo = createMockCardRepo();
+ const decksRouter = createDecksRouter({
+ deckRepo: mockDeckRepo,
+ cardRepo: mockCardRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/decks", decksRouter);
diff --git a/src/server/routes/decks.ts b/src/server/routes/decks.ts
index 2450bcd..2e170db 100644
--- a/src/server/routes/decks.ts
+++ b/src/server/routes/decks.ts
@@ -2,11 +2,17 @@ import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import { authMiddleware, Errors, getAuthUser } from "../middleware/index.js";
-import { type DeckRepository, deckRepository } from "../repositories/index.js";
+import {
+ type CardRepository,
+ cardRepository,
+ type DeckRepository,
+ deckRepository,
+} from "../repositories/index.js";
import { createDeckSchema, updateDeckSchema } from "../schemas/index.js";
export interface DeckDependencies {
deckRepo: DeckRepository;
+ cardRepo: CardRepository;
}
const deckIdParamSchema = z.object({
@@ -14,14 +20,21 @@ const deckIdParamSchema = z.object({
});
export function createDecksRouter(deps: DeckDependencies) {
- const { deckRepo } = deps;
+ const { deckRepo, cardRepo } = deps;
return new Hono()
.use("*", authMiddleware)
.get("/", async (c) => {
const user = getAuthUser(c);
const decks = await deckRepo.findByUserId(user.id);
- return c.json({ decks }, 200);
+ const now = new Date();
+ const decksWithDueCount = await Promise.all(
+ decks.map(async (deck) => {
+ const dueCardCount = await cardRepo.countDueCards(deck.id, now);
+ return { ...deck, dueCardCount };
+ }),
+ );
+ return c.json({ decks: decksWithDueCount }, 200);
})
.post("/", zValidator("json", createDeckSchema), async (c) => {
const user = getAuthUser(c);
@@ -79,4 +92,5 @@ export function createDecksRouter(deps: DeckDependencies) {
export const decks = createDecksRouter({
deckRepo: deckRepository,
+ cardRepo: cardRepository,
});
diff --git a/src/server/routes/study.test.ts b/src/server/routes/study.test.ts
index e2fb457..a5ac817 100644
--- a/src/server/routes/study.test.ts
+++ b/src/server/routes/study.test.ts
@@ -25,6 +25,7 @@ function createMockCardRepo(): CardRepository {
softDelete: vi.fn(),
softDeleteByNoteId: vi.fn(),
findDueCards: vi.fn(),
+ countDueCards: vi.fn(),
findDueCardsWithNoteData: vi.fn(),
findDueCardsForStudy: vi.fn(),
updateFSRSFields: vi.fn(),