Files
Momento/memento-note/app/api/notes/[id]/route.ts
sepehr b92f6384a4
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m11s
fix: chat memory lost between messages + per-note history
Chat (AIChat floating widget): conversationId was never captured from
the API response, so every message created a new conversation with no
context. Now creates the conversation upfront before streaming (same
pattern as ChatContainer) so the ID persists across messages.

Note history: was stored globally in UserAISettings, so enabling
history on one note enabled it for ALL notes. Now each Note has its
own historyEnabled boolean field. The "Enable history" action only
affects the specific note. A migration adds the column with default
false.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:18:46 +02:00

234 lines
5.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { parseNote } from '@/lib/utils'
import {
createNoteHistorySnapshot,
getNoteHistoryMode,
isNoteHistoryEnabledForUser,
shouldCaptureHistorySnapshot,
shouldCreateAutoSnapshot,
} from '@/lib/note-history'
// GET /api/notes/[id] - Get a single note
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
try {
const { id } = await params
const note = await prisma.note.findUnique({
where: { id }
})
if (!note) {
return NextResponse.json(
{ success: false, error: 'Note not found' },
{ status: 404 }
)
}
if (note.userId !== session.user.id) {
const share = await prisma.noteShare.findUnique({
where: {
noteId_userId: {
noteId: note.id,
userId: session.user.id
}
}
})
if (!share || share.status !== 'accepted') {
return NextResponse.json(
{ success: false, error: 'Forbidden' },
{ status: 403 }
)
}
}
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (error) {
console.error('Error fetching note:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch note' },
{ status: 500 }
)
}
}
// PUT /api/notes/[id] - Update a note
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
try {
const { id } = await params
const existingNote = await prisma.note.findUnique({
where: { id }
})
if (!existingNote) {
return NextResponse.json(
{ success: false, error: 'Note not found' },
{ status: 404 }
)
}
if (existingNote.userId !== session.user.id) {
return NextResponse.json(
{ success: false, error: 'Forbidden' },
{ status: 403 }
)
}
const body = await request.json()
// Whitelist allowed fields to prevent mass assignment
const allowedFields = ['title', 'content', 'color', 'isPinned', 'isArchived', 'type', 'isMarkdown', 'size', 'notebookId']
const updateData: Record<string, any> = {}
for (const key of allowedFields) {
if (key in body) {
updateData[key] = body[key]
}
}
if ('checkItems' in body) {
updateData.checkItems = body.checkItems ?? null
}
if ('labels' in body) {
updateData.labels = body.labels ?? null
}
// Only update if data actually changed
const hasChanges = Object.keys(updateData).some((key) => {
const newValue = updateData[key]
const oldValue = (existingNote as any)[key]
// Handle arrays/objects by comparing JSON
if (typeof newValue === 'object' && newValue !== null) {
return JSON.stringify(newValue) !== JSON.stringify(oldValue)
}
return newValue !== oldValue
})
// If no changes, return existing note without updating timestamp
if (!hasChanges) {
return NextResponse.json({
success: true,
data: parseNote(existingNote),
})
}
const note = await prisma.note.update({
where: { id },
data: updateData,
})
try {
const historyEnabled = existingNote.historyEnabled === true
if (historyEnabled && shouldCaptureHistorySnapshot(updateData)) {
const mode = await getNoteHistoryMode(session.user.id)
if (mode === 'auto') {
const shouldAuto = await shouldCreateAutoSnapshot({
noteId: id,
userId: session.user.id,
updateData,
existingContent: existingNote.content ?? '',
existingTitle: existingNote.title ?? null,
})
if (shouldAuto) {
await createNoteHistorySnapshot({
noteId: id,
userId: session.user.id,
reason: 'api:update',
})
}
}
// manual mode: no auto-snapshot
}
} catch (snapshotError) {
console.error('[HISTORY] Failed to create snapshot from /api/notes/[id] PUT:', snapshotError)
}
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (error) {
console.error('Error updating note:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update note' },
{ status: 500 }
)
}
}
// DELETE /api/notes/[id] - Delete a note
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
try {
const { id } = await params
const existingNote = await prisma.note.findUnique({
where: { id }
})
if (!existingNote) {
return NextResponse.json(
{ success: false, error: 'Note not found' },
{ status: 404 }
)
}
if (existingNote.userId !== session.user.id) {
return NextResponse.json(
{ success: false, error: 'Forbidden' },
{ status: 403 }
)
}
await prisma.note.update({
where: { id },
data: { trashedAt: new Date() }
})
return NextResponse.json({
success: true,
message: 'Note moved to trash'
})
} catch (error) {
console.error('Error deleting note:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete note' },
{ status: 500 }
)
}
}