From a448f5ef8625265df0ce572fcee57a6cea738e13 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 15 Jun 2026 20:05:33 -0600 Subject: [PATCH] feat: add CRUD routes for teaching design books --- server/routes/books.test.ts | 140 ++++++++++++++++++++++++++++++++++++ server/routes/books.ts | 62 ++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 server/routes/books.test.ts create mode 100644 server/routes/books.ts diff --git a/server/routes/books.test.ts b/server/routes/books.test.ts new file mode 100644 index 0000000..69bfcb3 --- /dev/null +++ b/server/routes/books.test.ts @@ -0,0 +1,140 @@ +import { beforeEach, describe, expect, it } from 'bun:test' +import type { Database } from 'bun:sqlite' +import { Hono } from 'hono' +import { createEmptyBook } from '../../src/domain/teachingDesign' +import { openDb } from '../db' +import { createBooksRouter } from './books' + +describe('books routes', () => { + let app: Hono + let db: Database + + beforeEach(() => { + db = openDb(':memory:') + app = new Hono().route('/api/books', createBooksRouter(db)) + }) + + async function createViaApi(name: string): Promise<{ id: string }> { + const res = await app.request('/api/books', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }) + return (await res.json()) as { id: string } + } + + it('lists no books initially', async () => { + const res = await app.request('/api/books') + expect(res.status).toBe(200) + expect(await res.json()).toEqual([]) + }) + + it('creates a book', async () => { + const res = await app.request('/api/books', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: '示例整本' }), + }) + expect(res.status).toBe(200) + + const body = (await res.json()) as { name: string; data: { designs: unknown[] } } + expect(body.name).toBe('示例整本') + expect(body.data.designs).toEqual([]) + }) + + it('returns 400 when creating without a name', async () => { + const res = await app.request('/api/books', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }) + expect(res.status).toBe(400) + }) + + it('gets a created book', async () => { + const created = await createViaApi('示例整本') + + const res = await app.request(`/api/books/${created.id}`) + expect(res.status).toBe(200) + expect(((await res.json()) as { id: string }).id).toBe(created.id) + }) + + it('returns 404 for a missing book', async () => { + const res = await app.request('/api/books/missing') + expect(res.status).toBe(404) + }) + + it('saves book data', async () => { + const created = await createViaApi('示例整本') + + const data = createEmptyBook() + data.cover.courseName = 'Web 前端开发' + + const res = await app.request(`/api/books/${created.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data }), + }) + expect(res.status).toBe(200) + + const fetched = await app.request(`/api/books/${created.id}`) + const body = (await fetched.json()) as { data: { cover: { courseName: string } } } + expect(body.data.cover.courseName).toBe('Web 前端开发') + }) + + it('returns 404 when saving data for a missing book', async () => { + const res = await app.request('/api/books/missing', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: createEmptyBook() }), + }) + expect(res.status).toBe(404) + }) + + it('returns 400 when saving without data', async () => { + const created = await createViaApi('示例整本') + + const res = await app.request(`/api/books/${created.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }) + expect(res.status).toBe(400) + }) + + it('renames a book', async () => { + const created = await createViaApi('旧名称') + + const res = await app.request(`/api/books/${created.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: '新名称' }), + }) + expect(res.status).toBe(200) + expect(((await res.json()) as { name: string }).name).toBe('新名称') + }) + + it('returns 404 when renaming a missing book', async () => { + const res = await app.request('/api/books/missing', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: '新名称' }), + }) + expect(res.status).toBe(404) + }) + + it('deletes a book', async () => { + const created = await createViaApi('示例整本') + + const res = await app.request(`/api/books/${created.id}`, { method: 'DELETE' }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ ok: true }) + + expect((await app.request(`/api/books/${created.id}`)).status).toBe(404) + }) + + it('returns 404 when deleting a missing book', async () => { + const res = await app.request('/api/books/missing', { method: 'DELETE' }) + expect(res.status).toBe(404) + }) +}) diff --git a/server/routes/books.ts b/server/routes/books.ts new file mode 100644 index 0000000..f31a3d2 --- /dev/null +++ b/server/routes/books.ts @@ -0,0 +1,62 @@ +import type { Database } from 'bun:sqlite' +import { Hono } from 'hono' +import type { TeachingBook } from '../../src/domain/teachingDesign' +import { createBook, deleteBook, getBook, listBooks, renameBook, saveBookData } from '../db' + +export function createBooksRouter(db: Database): Hono { + const app = new Hono() + + app.get('/', (c) => { + return c.json(listBooks(db)) + }) + + app.post('/', async (c) => { + const body = (await c.req.json().catch(() => null)) as { name?: unknown } | null + const name = body?.name + + if (typeof name !== 'string' || name.trim() === '') { + return c.json({ error: '请提供整本名称。' }, 400) + } + + return c.json(createBook(db, name.trim())) + }) + + app.get('/:id', (c) => { + const book = getBook(db, c.req.param('id')) + if (!book) return c.json({ error: '整本不存在。' }, 404) + return c.json(book) + }) + + app.put('/:id', async (c) => { + const body = (await c.req.json().catch(() => null)) as { data?: TeachingBook } | null + if (!body?.data) { + return c.json({ error: '请提供整本数据。' }, 400) + } + + const result = saveBookData(db, c.req.param('id'), body.data) + if (!result) return c.json({ error: '整本不存在。' }, 404) + return c.json(result) + }) + + app.patch('/:id', async (c) => { + const body = (await c.req.json().catch(() => null)) as { name?: unknown } | null + const name = body?.name + + if (typeof name !== 'string' || name.trim() === '') { + return c.json({ error: '请提供整本名称。' }, 400) + } + + const result = renameBook(db, c.req.param('id'), name.trim()) + if (!result) return c.json({ error: '整本不存在。' }, 404) + return c.json(result) + }) + + app.delete('/:id', (c) => { + if (!deleteBook(db, c.req.param('id'))) { + return c.json({ error: '整本不存在。' }, 404) + } + return c.json({ ok: true }) + }) + + return app +}