79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
|
|
import type { FastifyInstance } from 'fastify'
|
||
|
|
import { z } from 'zod'
|
||
|
|
import { db } from '../db/index.js'
|
||
|
|
|
||
|
|
const bookmarkSchema = z.object({
|
||
|
|
categoryId: z.number().int().nullable().optional(),
|
||
|
|
title: z.string().min(1).max(128),
|
||
|
|
url: z.string().url(),
|
||
|
|
icon: z.string().optional(),
|
||
|
|
favorite: z.boolean().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
const categorySchema = z.object({
|
||
|
|
name: z.string().min(1).max(64),
|
||
|
|
icon: z.string().optional(),
|
||
|
|
sortOrder: z.number().int().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
export async function bookmarkRoutes(app: FastifyInstance) {
|
||
|
|
app.addHook('onRequest', app.authenticate)
|
||
|
|
|
||
|
|
app.get('/api/bookmarks/categories', async () => {
|
||
|
|
const categories = db.prepare('SELECT * FROM bookmark_categories ORDER BY sort_order').all()
|
||
|
|
return { categories }
|
||
|
|
})
|
||
|
|
|
||
|
|
app.post('/api/bookmarks/categories', async (req, reply) => {
|
||
|
|
const parsed = categorySchema.safeParse(req.body)
|
||
|
|
if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
|
||
|
|
const { name, icon, sortOrder } = parsed.data
|
||
|
|
const result = db
|
||
|
|
.prepare('INSERT INTO bookmark_categories (name, icon, sort_order) VALUES (?, ?, ?)')
|
||
|
|
.run(name, icon ?? null, sortOrder ?? 0)
|
||
|
|
return reply.code(201).send({ id: result.lastInsertRowid })
|
||
|
|
})
|
||
|
|
|
||
|
|
app.get('/api/bookmarks', async () => {
|
||
|
|
const bookmarks = db.prepare('SELECT * FROM bookmarks ORDER BY created_at DESC').all()
|
||
|
|
return { bookmarks }
|
||
|
|
})
|
||
|
|
|
||
|
|
app.post('/api/bookmarks', async (req, reply) => {
|
||
|
|
const parsed = bookmarkSchema.safeParse(req.body)
|
||
|
|
if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
|
||
|
|
const { categoryId, title, url, icon, favorite } = parsed.data
|
||
|
|
const result = db
|
||
|
|
.prepare(
|
||
|
|
'INSERT INTO bookmarks (category_id, title, url, icon, favorite) VALUES (?, ?, ?, ?, ?)'
|
||
|
|
)
|
||
|
|
.run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0)
|
||
|
|
return reply.code(201).send({ id: result.lastInsertRowid })
|
||
|
|
})
|
||
|
|
|
||
|
|
app.put('/api/bookmarks/:id', async (req, reply) => {
|
||
|
|
const id = Number((req.params as { id: string }).id)
|
||
|
|
const parsed = bookmarkSchema.partial().safeParse(req.body)
|
||
|
|
if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
|
||
|
|
const existing = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id) as
|
||
|
|
| { category_id: number | null; title: string; url: string; icon: string | null; favorite: number }
|
||
|
|
| undefined
|
||
|
|
if (!existing) return reply.code(404).send({ error: 'Not found' })
|
||
|
|
const categoryId = parsed.data.categoryId ?? existing.category_id
|
||
|
|
const title = parsed.data.title ?? existing.title
|
||
|
|
const url = parsed.data.url ?? existing.url
|
||
|
|
const icon = parsed.data.icon ?? existing.icon
|
||
|
|
const favorite = parsed.data.favorite ?? !!existing.favorite
|
||
|
|
db.prepare(
|
||
|
|
'UPDATE bookmarks SET category_id = ?, title = ?, url = ?, icon = ?, favorite = ? WHERE id = ?'
|
||
|
|
).run(categoryId, title, url, icon, favorite ? 1 : 0, id)
|
||
|
|
return { ok: true }
|
||
|
|
})
|
||
|
|
|
||
|
|
app.delete('/api/bookmarks/:id', async (req, reply) => {
|
||
|
|
const id = Number((req.params as { id: string }).id)
|
||
|
|
db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id)
|
||
|
|
return reply.code(204).send()
|
||
|
|
})
|
||
|
|
}
|