feat: remove cover page
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { existsSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, setSystemTime } from 'bun:test'
|
||||
import { createEmptyBook, createEmptyTeachingDesign } from '../src/domain/teachingDesign'
|
||||
import {
|
||||
@@ -10,6 +13,12 @@ afterEach(() => {
|
||||
setSystemTime()
|
||||
})
|
||||
|
||||
function tempDbPath(name: string): string {
|
||||
const path = join(tmpdir(), `fake-teaching-design-${name}-${crypto.randomUUID()}.db`)
|
||||
if (existsSync(path)) rmSync(path)
|
||||
return path
|
||||
}
|
||||
|
||||
describe('db', () => {
|
||||
it('creates a book with empty data', () => {
|
||||
const db = openDb(':memory:')
|
||||
@@ -55,13 +64,85 @@ describe('db', () => {
|
||||
const db = openDb(':memory:')
|
||||
const created = createBook(db, '示例整本')
|
||||
const data = createEmptyBook()
|
||||
data.cover.courseName = 'Web 前端开发'
|
||||
data.designs.push(createEmptyTeachingDesign('1.md'))
|
||||
|
||||
setSystemTime(new Date('2026-02-01T00:00:00.000Z'))
|
||||
const result = saveBookData(db, created.id, data)
|
||||
|
||||
expect(result).toEqual({ id: created.id, name: '示例整本', updatedAt: '2026-02-01T00:00:00.000Z' })
|
||||
expect(getBook(db, created.id)?.data.cover.courseName).toBe('Web 前端开发')
|
||||
expect(getBook(db, created.id)?.data).not.toHaveProperty('cover')
|
||||
})
|
||||
|
||||
it('migrates legacy cover data and cover selection on open', () => {
|
||||
const path = tempDbPath('cover-migration')
|
||||
const db = openDb(path)
|
||||
const design = createEmptyTeachingDesign('1.md')
|
||||
const legacy = {
|
||||
schemaVersion: 1,
|
||||
cover: { courseName: '旧课程', teacherName: '旧教师' },
|
||||
designs: [design],
|
||||
selectedId: 'cover',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
}
|
||||
db.run('INSERT INTO books (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [
|
||||
'legacy-1',
|
||||
'旧整本',
|
||||
JSON.stringify(legacy),
|
||||
'2026-01-01T00:00:00.000Z',
|
||||
'2026-01-01T00:00:00.000Z',
|
||||
])
|
||||
db.close()
|
||||
|
||||
const reopened = openDb(path)
|
||||
const migrated = getBook(reopened, 'legacy-1')!.data
|
||||
const raw = reopened.query<{ data: string }, [string]>('SELECT data FROM books WHERE id = ?').get('legacy-1')!.data
|
||||
reopened.close()
|
||||
rmSync(path)
|
||||
|
||||
expect(migrated).not.toHaveProperty('cover')
|
||||
expect(migrated.selectedId).toBe(design.id)
|
||||
expect(JSON.parse(raw)).not.toHaveProperty('cover')
|
||||
})
|
||||
|
||||
it('migrates legacy cover selection to null when no lessons exist', () => {
|
||||
const path = tempDbPath('empty-cover-migration')
|
||||
const db = openDb(path)
|
||||
db.run('INSERT INTO books (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [
|
||||
'legacy-empty',
|
||||
'空整本',
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
cover: { courseName: '旧课程', teacherName: '旧教师' },
|
||||
designs: [],
|
||||
selectedId: 'cover',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
}),
|
||||
'2026-01-01T00:00:00.000Z',
|
||||
'2026-01-01T00:00:00.000Z',
|
||||
])
|
||||
db.close()
|
||||
|
||||
const reopened = openDb(path)
|
||||
const migrated = getBook(reopened, 'legacy-empty')!.data
|
||||
reopened.close()
|
||||
rmSync(path)
|
||||
|
||||
expect(migrated).not.toHaveProperty('cover')
|
||||
expect(migrated.selectedId).toBeNull()
|
||||
})
|
||||
|
||||
it('normalizes invalid selected ids to the first lesson', () => {
|
||||
const db = openDb(':memory:')
|
||||
const created = createBook(db, '示例整本')
|
||||
const data = createEmptyBook()
|
||||
const design = createEmptyTeachingDesign('1.md')
|
||||
data.designs.push(design)
|
||||
db.run('UPDATE books SET data = ? WHERE id = ?', [
|
||||
JSON.stringify({ ...data, selectedId: 'missing-id' }),
|
||||
created.id,
|
||||
])
|
||||
|
||||
expect(getBook(db, created.id)?.data.selectedId).toBe(design.id)
|
||||
})
|
||||
|
||||
it('returns null when saving data for a missing book', () => {
|
||||
|
||||
66
server/db.ts
66
server/db.ts
@@ -28,6 +28,16 @@ interface BookRow {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type StoredTeachingBook = Omit<TeachingBook, 'selectedId'> & {
|
||||
cover?: unknown
|
||||
selectedId?: string | null
|
||||
}
|
||||
|
||||
interface NormalizedBookData {
|
||||
data: TeachingBook
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
export interface UserRecord {
|
||||
id: string
|
||||
username: string
|
||||
@@ -77,10 +87,59 @@ const SCHEMA = `
|
||||
)
|
||||
`
|
||||
|
||||
function normalizeBookData(raw: StoredTeachingBook): NormalizedBookData {
|
||||
const data = { ...raw, designs: Array.isArray(raw.designs) ? raw.designs : [] }
|
||||
let changed = false
|
||||
|
||||
if ('cover' in data) {
|
||||
delete data.cover
|
||||
changed = true
|
||||
}
|
||||
|
||||
const selectedId = data.selectedId ?? null
|
||||
const firstDesignId = data.designs[0]?.id ?? null
|
||||
const selectedExists =
|
||||
selectedId !== null && data.designs.some((design) => design.id === selectedId)
|
||||
|
||||
let normalizedSelectedId: TeachingBook['selectedId']
|
||||
if (selectedId === 'cover' || (selectedId !== null && !selectedExists)) {
|
||||
normalizedSelectedId = firstDesignId
|
||||
changed = selectedId !== normalizedSelectedId || changed
|
||||
} else {
|
||||
normalizedSelectedId = selectedId as TeachingBook['selectedId']
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
schemaVersion: data.schemaVersion,
|
||||
designs: data.designs,
|
||||
selectedId: normalizedSelectedId,
|
||||
updatedAt: data.updatedAt,
|
||||
},
|
||||
changed,
|
||||
}
|
||||
}
|
||||
|
||||
function parseBookData(data: string): TeachingBook {
|
||||
return normalizeBookData(JSON.parse(data) as StoredTeachingBook).data
|
||||
}
|
||||
|
||||
function migrateStoredBooks(db: Database): void {
|
||||
const rows = db.query<{ id: string; data: string }, []>('SELECT id, data FROM books').all()
|
||||
|
||||
for (const row of rows) {
|
||||
const normalized = normalizeBookData(JSON.parse(row.data) as StoredTeachingBook)
|
||||
if (normalized.changed) {
|
||||
db.run('UPDATE books SET data = ? WHERE id = ?', [JSON.stringify(normalized.data), row.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function openDb(path: string): Database {
|
||||
const db = new Database(path)
|
||||
db.run('PRAGMA foreign_keys = ON')
|
||||
db.run(SCHEMA)
|
||||
migrateStoredBooks(db)
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -93,7 +152,7 @@ export function listBooks(db: Database): BookSummary[] {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
updatedAt: row.updated_at,
|
||||
lessonCount: (JSON.parse(row.data) as TeachingBook).designs.length,
|
||||
lessonCount: parseBookData(row.data).designs.length,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -124,7 +183,7 @@ export function getBook(db: Database, id: string): BookRecord | null {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
updatedAt: row.updated_at,
|
||||
data: JSON.parse(row.data) as TeachingBook,
|
||||
data: parseBookData(row.data),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +194,8 @@ export function saveBookData(db: Database, id: string, data: TeachingBook): Book
|
||||
if (!existing) return null
|
||||
|
||||
const now = new Date().toISOString()
|
||||
db.run('UPDATE books SET data = ?, updated_at = ? WHERE id = ?', [JSON.stringify(data), now, id])
|
||||
const normalized = normalizeBookData(data as unknown as StoredTeachingBook).data
|
||||
db.run('UPDATE books SET data = ?, updated_at = ? WHERE id = ?', [JSON.stringify(normalized), now, id])
|
||||
|
||||
return { id, name: existing.name, updatedAt: now }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { createEmptyBook, createEmptyTeachingDesign } from '../../src/domain/teachingDesign'
|
||||
import { openDb } from '../db'
|
||||
import { createBooksRouter } from './books'
|
||||
|
||||
@@ -64,22 +64,22 @@ describe('books routes', () => {
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
it('saves book data', async () => {
|
||||
it('saves book data without cover state', async () => {
|
||||
const created = await createViaApi('示例整本')
|
||||
|
||||
const data = createEmptyBook()
|
||||
data.cover.courseName = 'Web 前端开发'
|
||||
data.designs.push(createEmptyTeachingDesign('1.md'))
|
||||
|
||||
const res = await app.request(`/api/books/${created.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data }),
|
||||
body: JSON.stringify({ data: { ...data, cover: { courseName: '旧课程', teacherName: '旧教师' } } }),
|
||||
})
|
||||
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 前端开发')
|
||||
const body = (await fetched.json()) as { data: Record<string, unknown> }
|
||||
expect(body.data).not.toHaveProperty('cover')
|
||||
})
|
||||
|
||||
it('returns 404 when saving data for a missing book', async () => {
|
||||
|
||||
Reference in New Issue
Block a user