diff --git a/backend/src/modules/notes/routes.ts b/backend/src/modules/notes/routes.ts index 0caa277..8160513 100644 --- a/backend/src/modules/notes/routes.ts +++ b/backend/src/modules/notes/routes.ts @@ -40,14 +40,17 @@ const ChatBodySchema = z.object({ }); function toLexicalHits(items: NoteDoc[]) { - return items.map((n) => ({ - noteId: n.id, - workspaceId: n.workspaceId, - title: n.title, - score: 1, - matchKind: 'lexical' as const, - snippet: n.body.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 180) + (n.body.length > 180 ? '…' : ''), - })); + return items.map((n) => { + const plain = n.body.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); + return { + noteId: n.id, + workspaceId: n.workspaceId, + title: n.title, + score: 1, + matchKind: 'lexical' as const, + snippet: plain.slice(0, 180) + (plain.length > 180 ? '…' : ''), + }; + }); } export async function noteRoutes(app: RouteApp) { @@ -81,6 +84,33 @@ export async function noteRoutes(app: RouteApp) { return { ...result, limit: parsed.data.limit, offset: parsed.data.offset }; }); + // Must be registered before GET /notes/:id or "export" is captured as :id. + app.get('/notes/export', async (req, reply) => { + const auth = await extractAuth(req); + const query = req.query as { format?: string; workspaceId?: string }; + const format = query.format ?? 'json'; + + if (format !== 'json' && format !== 'markdown') { + throw new BadRequestError('format must be json or markdown'); + } + + const listQuery = { workspaceId: query.workspaceId, limit: 100, offset: 0 }; + const result = await repo.listNotes(auth.sub, PRODUCT_ID, listQuery); + + if (format === 'markdown') { + const md = result.items + .map((n: NoteDoc) => `# ${n.title}\n\n${n.body}\n\n---\n`) + .join('\n'); + reply.header('Content-Type', 'text/markdown'); + reply.header('Content-Disposition', 'attachment; filename="notes-export.md"'); + return md; + } + + reply.header('Content-Type', 'application/json'); + reply.header('Content-Disposition', 'attachment; filename="notes-export.json"'); + return { exportedAt: new Date().toISOString(), notes: result.items }; + }); + app.get('/notes/:id', async req => { const auth = await extractAuth(req); const { id } = req.params as { id: string }; @@ -470,32 +500,6 @@ export async function noteRoutes(app: RouteApp) { return { answer, citations }; }); - app.get('/notes/export', async (req, reply) => { - const auth = await extractAuth(req); - const query = req.query as { format?: string; workspaceId?: string }; - const format = query.format ?? 'json'; - - if (format !== 'json' && format !== 'markdown') { - throw new BadRequestError('format must be json or markdown'); - } - - const listQuery = { workspaceId: query.workspaceId, limit: 100, offset: 0 }; - const result = await repo.listNotes(auth.sub, PRODUCT_ID, listQuery); - - if (format === 'markdown') { - const md = result.items - .map((n: NoteDoc) => `# ${n.title}\n\n${n.body}\n\n---\n`) - .join('\n'); - reply.header('Content-Type', 'text/markdown'); - reply.header('Content-Disposition', 'attachment; filename="notes-export.md"'); - return md; - } - - reply.header('Content-Type', 'application/json'); - reply.header('Content-Disposition', 'attachment; filename="notes-export.json"'); - return { exportedAt: new Date().toISOString(), notes: result.items }; - }); - app.delete('/notes/:id', async (req, reply) => { const auth = await requireWriter(req); const { id } = req.params as { id: string };