Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/routes/books/[id]/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Layout } from "../../../components/layout/Layout";
import { Input, Textarea, Label } from "../../../components/ui/Input";
import { Button } from "../../../components/ui/Button";
import { bookRepo } from "../../../server/db/repositories";
import type { BookId } from "../../../types/domain";

export default createRoute(async (c) => {
const authResult = await requirePageAuth(c);
Expand All @@ -13,7 +14,7 @@ export default createRoute(async (c) => {
const user = authResult;
const sidebarExpanded = getSidebarExpanded(c);

const id = c.req.param("id")!;
const id = c.req.param("id")! as BookId;

let book;
try {
Expand Down
3 changes: 2 additions & 1 deletion app/routes/books/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Layout } from "../../../components/layout/Layout";
import { BookCover } from "../../../components/book/BookCover";
import { Button } from "../../../components/ui/Button";
import { bookRepo } from "../../../server/db/repositories";
import type { BookId } from "../../../types/domain";
import ProgressSlider from "../../../islands/ProgressSlider";
import StatusToggle from "../../../islands/StatusToggle";
import MemoEditor from "../../../islands/MemoEditor";
Expand All @@ -16,7 +17,7 @@ export default createRoute(async (c) => {
const user = authResult;
const sidebarExpanded = getSidebarExpanded(c);

const id = c.req.param("id")!;
const id = c.req.param("id")! as BookId;

let book;
try {
Expand Down
3 changes: 2 additions & 1 deletion app/server/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Hono } from "hono";
import { setCookie, deleteCookie, getCookie } from "hono/cookie";
import type { HonoContext } from "../../types/env";
import type { UserId } from "../../types/domain";
import { userRepo } from "../db/repositories";
import { authMiddleware } from "../lib/auth";
import { validator, getValidated } from "../lib/validator";
Expand Down Expand Up @@ -102,7 +103,7 @@
// 新規ユーザー作成
const now = new Date().toISOString();
user = await userRepo.create(c.env.DB, {
id: crypto.randomUUID(),
id: crypto.randomUUID() as UserId,
username: await generateUniqueUsername(c.env.DB, oauthUser.username),
email: oauthUser.email,
name: oauthUser.name,
Expand Down Expand Up @@ -146,7 +147,7 @@
let candidate = username;
let counter = 1;

while (await userRepo.isUsernameTaken(db, candidate)) {

Check warning on line 150 in app/server/api/auth.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-await-in-loop)

Unexpected `await` inside a loop.

Check warning on line 150 in app/server/api/auth.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-await-in-loop)

Unexpected `await` inside a loop.
candidate = `${username}${counter}`;
counter++;
if (counter > 1000) {
Expand Down
5 changes: 3 additions & 2 deletions app/server/api/books.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import api from "./index";
import { createTestUser, createTestSession, createTestBook } from "../../test/helpers";
import type { SuccessResponse, PaginatedResponse } from "../lib/response";
import type { BookResponse } from "../../types/database";
import type { UserId, SessionId } from "../../types/domain";

describe("Books API Integration", () => {
let userId: string;
let sessionId: string;
let userId: UserId;
let sessionId: SessionId;

beforeEach(async () => {
await env.DB.prepare("DELETE FROM book_tags").run();
Expand Down
17 changes: 7 additions & 10 deletions app/server/api/books.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Hono } from "hono";
import type { HonoContext } from "../../types/env";
import type { BookId } from "../../types/domain";
import { bookRepo, bookTagRepo } from "../db/repositories";
import { authMiddleware } from "../lib/auth";
import { validator, getValidated } from "../lib/validator";
Expand Down Expand Up @@ -46,10 +47,6 @@ app.get("/", validator("query", bookFilterSchema), async (c) => {
return paginatedResponse(c, items, total, filter.limit ?? 20, filter.offset ?? 0);
});

/**
* 書籍詳細取得
* GET /api/books/:id
*/
/**
* 書籍統計取得
* GET /api/books/stats
Expand All @@ -69,8 +66,8 @@ app.get("/:id", validator("param", bookIdSchema), async (c) => {
const userId = c.get("userId");
const { id } = getValidated<{ id: string }>(c, "param");

const book = await bookRepo.findById(c.env.DB, id, userId);
const tags = await bookTagRepo.findTagsByBookId(c.env.DB, id);
const book = await bookRepo.findById(c.env.DB, id as BookId, userId);
const tags = await bookTagRepo.findTagsByBookId(c.env.DB, id as BookId);

return successResponse(c, {
...toBookResponse(book),
Expand Down Expand Up @@ -106,7 +103,7 @@ app.put(
const data = getValidated<UpdateBookInput>(c, "json");

const updateData = toBookUpdateInput(data);
const book = await bookRepo.update(c.env.DB, id, userId, updateData);
const book = await bookRepo.update(c.env.DB, id as BookId, userId, updateData);

return successResponse(c, toBookResponse(book));
},
Expand All @@ -126,7 +123,7 @@ app.patch(
const { currentPage } = getValidated<ProgressInput>(c, "json");

// 書籍を取得してページ数を検証
const existingBook = await bookRepo.findById(c.env.DB, id, userId);
const existingBook = await bookRepo.findById(c.env.DB, id as BookId, userId);
const validation = bookDomain.validatePageProgress(currentPage, existingBook.page_count);

if (!validation.valid) {
Expand All @@ -141,7 +138,7 @@ app.patch(
? new Date().toISOString()
: existingBook.finished_at;

const book = await bookRepo.update(c.env.DB, id, userId, {
const book = await bookRepo.update(c.env.DB, id as BookId, userId, {
currentPage: currentPage,
status: newStatus,
finishedAt,
Expand All @@ -160,7 +157,7 @@ app.delete("/:id", validator("param", bookIdSchema), async (c) => {
const userId = c.get("userId");
const { id } = getValidated<{ id: string }>(c, "param");

await bookRepo.deleteById(c.env.DB, id, userId);
await bookRepo.deleteById(c.env.DB, id as BookId, userId);

return successResponse(c, { deleted: true });
});
Expand Down
5 changes: 3 additions & 2 deletions app/server/api/schemas/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { regex, type } from "arktype";

export type { OAuthProvider } from "../../../types/domain";

/**
* OAuth認証プロバイダー
*/
Expand All @@ -26,7 +28,7 @@ export const oauthCallbackSchema = type({
export const rakutenSearchSchema = type({
query: "1 <= string <= 100",
"limit?": "1 <= (number % 1) <= 10",
"page?": "1 <= (number % 1) <= 1000", // 追加
"page?": "1 <= (number % 1) <= 1000",
});

/**
Expand All @@ -39,7 +41,6 @@ export const isbnSearchSchema = type({
});

// 型エクスポート
export type OAuthProvider = typeof oauthProviderSchema.infer;
export type OAuthProviderParam = typeof oauthProviderParamSchema.infer;
export type OAuthCallbackInput = typeof oauthCallbackSchema.infer;
export type RakutenSearchInput = typeof rakutenSearchSchema.infer;
Expand Down
5 changes: 3 additions & 2 deletions app/server/api/tags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
} from "../../test/helpers";
import type { SuccessResponse } from "../lib/response";
import type { TagResponse } from "../../types/database";
import type { UserId, SessionId } from "../../types/domain";

describe("Tags API Integration", () => {
let userId: string;
let sessionId: string;
let userId: UserId;
let sessionId: SessionId;

beforeEach(async () => {
await env.DB.prepare("DELETE FROM book_tags").run();
Expand Down
11 changes: 6 additions & 5 deletions app/server/api/tags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Hono } from "hono";
import type { HonoContext } from "../../types/env";
import type { BookId, TagId } from "../../types/domain";
import { tagRepo, bookTagRepo } from "../db/repositories";
import { authMiddleware } from "../lib/auth";
import { validator, getValidated } from "../lib/validator";
Expand Down Expand Up @@ -53,7 +54,7 @@ app.put("/:id", validator("param", tagIdSchema), validator("json", updateTagSche
const { id } = getValidated<{ id: string }>(c, "param");
const data = getValidated<UpdateTagInput>(c, "json");

const tag = await tagRepo.update(c.env.DB, id, userId, data.name);
const tag = await tagRepo.update(c.env.DB, id as TagId, userId, data.name);

return successResponse(c, toTagResponse(tag));
});
Expand All @@ -66,7 +67,7 @@ app.delete("/:id", validator("param", tagIdSchema), async (c) => {
const userId = c.get("userId");
const { id } = getValidated<{ id: string }>(c, "param");

await tagRepo.deleteById(c.env.DB, id, userId);
await tagRepo.deleteById(c.env.DB, id as TagId, userId);

return successResponse(c, { deleted: true });
});
Expand All @@ -84,7 +85,7 @@ app.post(
const { bookId } = getValidated<{ bookId: string }>(c, "param");
const { tagId } = getValidated<AddTagToBookInput>(c, "json");

await bookTagRepo.addTagToBook(c.env.DB, bookId, tagId, userId);
await bookTagRepo.addTagToBook(c.env.DB, bookId as BookId, tagId as TagId, userId);

return successResponse(c, { added: true }, 201);
},
Expand All @@ -99,7 +100,7 @@ app.delete("/books/:bookId/:tagId", async (c) => {
const bookId = c.req.param("bookId");
const tagId = c.req.param("tagId");

await bookTagRepo.removeTagFromBook(c.env.DB, bookId, tagId, userId);
await bookTagRepo.removeTagFromBook(c.env.DB, bookId as BookId, tagId as TagId, userId);

return successResponse(c, { removed: true });
});
Expand All @@ -111,7 +112,7 @@ app.delete("/books/:bookId/:tagId", async (c) => {
app.get("/books/:bookId", async (c) => {
const bookId = c.req.param("bookId");

const tags = await bookTagRepo.findTagsByBookId(c.env.DB, bookId);
const tags = await bookTagRepo.findTagsByBookId(c.env.DB, bookId as BookId);

return successResponse(c, tags.map(toTagResponse));
});
Expand Down
11 changes: 6 additions & 5 deletions app/server/db/repositories/book.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import { env } from "cloudflare:test";
import * as bookRepo from "./book";
import { createTestUser, createTestBook } from "../../../test/helpers";
import type { UserId, BookId } from "../../../types/domain";

describe("Book Repository", () => {
let userId: string;
let userId: UserId;

beforeEach(async () => {
// テストデータをクリーンアップ
Expand All @@ -21,7 +22,7 @@
describe("create", () => {
it("should create a book", async () => {
const now = new Date().toISOString();
const bookId = crypto.randomUUID();
const bookId = crypto.randomUUID() as BookId;

const book = await bookRepo.create(env.DB, {
id: bookId,
Expand Down Expand Up @@ -51,9 +52,9 @@
});

it("should throw NotFoundError for non-existent book", async () => {
await expect(bookRepo.findById(env.DB, "non-existent", userId)).rejects.toThrow(
"Book not found",
);
await expect(
bookRepo.findById(env.DB, "non-existent" as BookId, userId),
).rejects.toThrow("Book not found");
});

it("should not return books from other users", async () => {
Expand Down Expand Up @@ -93,7 +94,7 @@

it("should support pagination", async () => {
for (let i = 0; i < 5; i++) {
await createTestBook(env.DB, userId, { title: `Book ${i}` });

Check warning on line 97 in app/server/db/repositories/book.test.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-await-in-loop)

Unexpected `await` inside a loop.

Check warning on line 97 in app/server/db/repositories/book.test.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-await-in-loop)

Unexpected `await` inside a loop.
}

const { books: page1 } = await bookRepo.findByUserId(env.DB, userId, {
Expand Down
24 changes: 13 additions & 11 deletions app/server/db/repositories/book.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Book, BookInput, BookFilter, BookStats, BookStatus } from "../../../types/database";
import type { Book, BookInput, BookFilter, BookStats } from "../../../types/database";
import type { BookStatus } from "../../../types/domain";
import { NotFoundError, DatabaseError } from "../../lib/errors";

import type { Env } from "../../../types/env";
import type { UserId, BookId } from "../../../types/domain";

type D1Database = Env["DB"];
type D1BindValue = string | number | null;
Expand All @@ -11,7 +13,7 @@ type D1BindValue = string | number | null;
*/
export async function findByUserId(
db: D1Database,
userId: string,
userId: UserId,
filter?: BookFilter & { limit?: number; offset?: number },
): Promise<{ books: Book[]; total: number }> {
try {
Expand Down Expand Up @@ -82,7 +84,7 @@ export async function findByUserId(
/**
* IDで書籍を取得
*/
export async function findById(db: D1Database, bookId: string, userId: string): Promise<Book> {
export async function findById(db: D1Database, bookId: BookId, userId: UserId): Promise<Book> {
try {
const result = await db
.prepare("SELECT * FROM books WHERE id = ? AND user_id = ?")
Expand Down Expand Up @@ -149,8 +151,8 @@ export async function create(db: D1Database, book: BookInput): Promise<Book> {
*/
export async function update(
db: D1Database,
bookId: string,
userId: string,
bookId: BookId,
userId: UserId,
data: Partial<BookInput>,
): Promise<Book> {
try {
Expand Down Expand Up @@ -210,8 +212,8 @@ export async function update(
*/
export async function updateProgress(
db: D1Database,
bookId: string,
userId: string,
bookId: BookId,
userId: UserId,
currentPage: number,
status: BookStatus,
): Promise<Book> {
Expand Down Expand Up @@ -240,7 +242,7 @@ export async function updateProgress(
/**
* 書籍を削除(関連するタグも削除)
*/
export async function deleteById(db: D1Database, bookId: string, userId: string): Promise<void> {
export async function deleteById(db: D1Database, bookId: BookId, userId: UserId): Promise<void> {
try {
// 書籍の存在確認
await findById(db, bookId, userId);
Expand All @@ -259,7 +261,7 @@ export async function deleteById(db: D1Database, bookId: string, userId: string)
/**
* ユーザーの統計情報を取得
*/
export async function getStats(db: D1Database, userId: string): Promise<BookStats> {
export async function getStats(db: D1Database, userId: UserId): Promise<BookStats> {
try {
const result = await db
.prepare(
Expand Down Expand Up @@ -294,8 +296,8 @@ export async function getStats(db: D1Database, userId: string): Promise<BookStat
*/
export async function belongsToUser(
db: D1Database,
bookId: string,
userId: string,
bookId: BookId,
userId: UserId,
): Promise<boolean> {
try {
const result = await db
Expand Down
19 changes: 10 additions & 9 deletions app/server/db/repositories/bookTag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
createTestUser,
cleanupDatabase,
} from "../../../test/helpers";
import type { UserId, BookId, TagId } from "../../../types/domain";

describe("BookTag Repository", () => {
let userId: string;
let bookId: string;
let tagId: string;
let userId: UserId;
let bookId: BookId;
let tagId: TagId;

beforeEach(async () => {
await cleanupDatabase(env.DB);
Expand Down Expand Up @@ -47,15 +48,15 @@ describe("BookTag Repository", () => {
});

it("should reject adding tag for missing book", async () => {
await expect(bookTagRepo.addTagToBook(env.DB, "missing-book", tagId, userId)).rejects.toThrow(
"Book not found",
);
await expect(
bookTagRepo.addTagToBook(env.DB, "missing-book" as BookId, tagId, userId),
).rejects.toThrow("Book not found");
});

it("should reject adding tag for missing tag", async () => {
await expect(bookTagRepo.addTagToBook(env.DB, bookId, "missing-tag", userId)).rejects.toThrow(
"Tag not found",
);
await expect(
bookTagRepo.addTagToBook(env.DB, bookId, "missing-tag" as TagId, userId),
).rejects.toThrow("Tag not found");
});

it("should reject adding tag for other user book", async () => {
Expand Down
Loading
Loading