feat: rename keep-notes to memento-note, migrate to PostgreSQL, fix MCP bugs

- Rename directory keep-notes -> memento-note with all code references
- Prisma: SQLite -> PostgreSQL (both app and MCP server schemas)
- Sync MCP schema with main app (add missing fields, relations, indexes)
- Delete 17 SQLite migrations (clean slate for PostgreSQL)
- Remove SQLite dependencies (@libsql/client, better-sqlite3, etc.)
- Fix MCP server: hardcoded Windows DB paths -> DATABASE_URL env var
- Fix MCP server: .dockerignore excluded index-sse.js (SSE mode broken)
- MCP Dockerfile: node:20 -> node:22
- Docker Compose: add postgres service, remove SQLite volume
- Generate favicon.ico, icon-192.png, icon-512.png, apple-icon.png
- Update layout.tsx icons and manifest.json for PNG icons
- Update all .env files for PostgreSQL
- Rewrite README.md with updated sections
- Remove mcp-server/node_modules and prisma/client-generated from git tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-20 20:58:04 +02:00
parent e2ddfa53d4
commit aa6a214f37
3548 changed files with 440 additions and 516800 deletions

View File

@@ -0,0 +1,90 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'
// POST /api/notes/[id]/move - Move a note to a notebook (or to Inbox)
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { id } = await params
const body = await request.json()
const { notebookId } = body
// Get the note
const note = await prisma.note.findUnique({
where: { id },
select: {
id: true,
userId: true,
notebookId: true
}
})
if (!note) {
return NextResponse.json(
{ success: false, error: 'Note not found' },
{ status: 404 }
)
}
// Verify ownership
if (note.userId !== session.user.id) {
return NextResponse.json(
{ success: false, error: 'Forbidden' },
{ status: 403 }
)
}
// If notebookId is provided, verify it exists and belongs to the user
if (notebookId !== null && notebookId !== '') {
const notebook = await prisma.notebook.findUnique({
where: { id: notebookId },
select: { userId: true }
})
if (!notebook || notebook.userId !== session.user.id) {
return NextResponse.json(
{ success: false, error: 'Notebook not found or unauthorized' },
{ status: 403 }
)
}
}
// Update the note's notebook
// notebookId = null or "" means move to Inbox (Notes générales)
const updatedNote = await prisma.note.update({
where: { id },
data: {
notebookId: notebookId && notebookId !== '' ? notebookId : null
},
include: {
notebook: {
select: { id: true, name: true }
}
}
})
revalidatePath('/')
return NextResponse.json({
success: true,
data: updatedNote,
message: notebookId && notebookId !== ''
? `Note moved to "${updatedNote.notebook?.name || 'notebook'}"`
: 'Note moved to Inbox'
})
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to move note' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
// Helper to parse JSON fields
function parseNote(dbNote: any) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
}
}
// GET /api/notes/[id] - Get a single note
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
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 }
)
}
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (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 }> }
) {
try {
const { id } = await params
const body = await request.json()
const updateData: any = { ...body }
// Stringify JSON fields if they exist
if ('checkItems' in body) {
updateData.checkItems = body.checkItems ? JSON.stringify(body.checkItems) : null
}
if ('labels' in body) {
updateData.labels = body.labels ? JSON.stringify(body.labels) : null
}
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id },
data: updateData
})
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (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 }> }
) {
try {
const { id } = await params
await prisma.note.delete({
where: { id }
})
return NextResponse.json({
success: true,
message: 'Note deleted successfully'
})
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to delete note' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function POST(req: NextRequest) {
try {
// Check authentication
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
// Delete all notes for the user (cascade will handle labels-note relationships)
const result = await prisma.note.deleteMany({
where: {
userId: session.user.id
}
})
// Delete all labels for the user
await prisma.label.deleteMany({
where: {
userId: session.user.id
}
})
// Delete all notebooks for the user
await prisma.notebook.deleteMany({
where: {
userId: session.user.id
}
})
// Revalidate paths
revalidatePath('/')
revalidatePath('/settings/data')
return NextResponse.json({
success: true,
deletedNotes: result.count
})
} catch (error) {
console.error('Delete all error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete notes' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(req: NextRequest) {
try {
// Check authentication
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
// Fetch all notes with related data
const notes = await prisma.note.findMany({
where: {
userId: session.user.id
},
include: {
labels: {
select: {
id: true,
name: true
}
},
notebook: {
select: {
id: true,
name: true
}
}
},
orderBy: {
createdAt: 'desc'
}
})
// Fetch labels separately
const labels = await prisma.label.findMany({
where: {
userId: session.user.id
},
include: {
notes: {
select: {
id: true
}
}
}
})
// Fetch notebooks
const notebooks = await prisma.notebook.findMany({
where: {
userId: session.user.id
},
include: {
notes: {
select: {
id: true
}
}
}
})
// Create export object
const exportData = {
version: '1.0.0',
exportDate: new Date().toISOString(),
user: {
id: session.user.id,
email: session.user.email,
name: session.user.name
},
data: {
labels: labels.map(label => ({
id: label.id,
name: label.name,
color: label.color,
noteCount: label.notes.length
})),
notebooks: notebooks.map(notebook => ({
id: notebook.id,
name: notebook.name,
description: notebook.description,
noteCount: notebook.notes.length
})),
notes: notes.map(note => ({
id: note.id,
title: note.title,
content: note.content,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
isPinned: note.isPinned,
notebookId: note.notebookId,
labels: note.labels.map(label => ({
id: label.id,
name: label.name
}))
}))
}
}
// Return as JSON file
const jsonString = JSON.stringify(exportData, null, 2)
return new NextResponse(jsonString, {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="memento-export-${new Date().toISOString().split('T')[0]}.json"`
}
})
} catch (error) {
console.error('Export error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to export notes' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,158 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function POST(req: NextRequest) {
try {
// Check authentication
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
// Parse form data
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ success: false, error: 'No file provided' },
{ status: 400 }
)
}
// Parse JSON file
const text = await file.text()
let importData: any
try {
importData = JSON.parse(text)
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Invalid JSON file' },
{ status: 400 }
)
}
// Validate import data structure
if (!importData.data || !importData.data.notes) {
return NextResponse.json(
{ success: false, error: 'Invalid import format' },
{ status: 400 }
)
}
let importedNotes = 0
let importedLabels = 0
let importedNotebooks = 0
// Import labels first
if (importData.data.labels && Array.isArray(importData.data.labels)) {
for (const label of importData.data.labels) {
// Check if label already exists
const existing = await prisma.label.findFirst({
where: {
userId: session.user.id,
name: label.name
}
})
if (!existing) {
await prisma.label.create({
data: {
userId: session.user.id,
name: label.name,
color: label.color
}
})
importedLabels++
}
}
}
// Import notebooks
const notebookIdMap = new Map<string, string>()
if (importData.data.notebooks && Array.isArray(importData.data.notebooks)) {
for (const notebook of importData.data.notebooks) {
// Check if notebook already exists
const existing = await prisma.notebook.findFirst({
where: {
userId: session.user.id,
name: notebook.name
}
})
let newNotebookId
if (!existing) {
const created = await prisma.notebook.create({
data: {
userId: session.user.id,
name: notebook.name,
description: notebook.description || null,
position: 0
}
})
newNotebookId = created.id
notebookIdMap.set(notebook.id, newNotebookId)
importedNotebooks++
} else {
notebookIdMap.set(notebook.id, existing.id)
}
}
}
// Import notes
if (importData.data.notes && Array.isArray(importData.data.notes)) {
for (const note of importData.data.notes) {
// Map notebook ID
const mappedNotebookId = notebookIdMap.get(note.notebookId) || null
// Get label IDs
const labels = await prisma.label.findMany({
where: {
userId: session.user.id,
name: {
in: note.labels.map((l: any) => l.name)
}
}
})
// Create note
await prisma.note.create({
data: {
userId: session.user.id,
title: note.title || 'Untitled',
content: note.content,
isPinned: note.isPinned || false,
notebookId: mappedNotebookId,
labels: {
connect: labels.map(label => ({ id: label.id }))
}
}
})
importedNotes++
}
}
// Revalidate paths
revalidatePath('/')
revalidatePath('/settings/data')
return NextResponse.json({
success: true,
count: importedNotes,
labels: importedLabels,
notebooks: importedNotebooks
})
} catch (error) {
console.error('Import error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to import notes' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,162 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { CheckItem } from '@/lib/types'
// Helper to parse JSON fields
function parseNote(dbNote: any) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
}
}
// GET /api/notes - Get all notes
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const includeArchived = searchParams.get('archived') === 'true'
const search = searchParams.get('search')
let where: any = {}
if (!includeArchived) {
where.isArchived = false
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } }
]
}
const notes = await prisma.note.findMany({
where,
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
{ updatedAt: 'desc' }
]
})
return NextResponse.json({
success: true,
data: notes.map(parseNote)
})
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to fetch notes' },
{ status: 500 }
)
}
}
// POST /api/notes - Create a new note
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { title, content, color, type, checkItems, labels, images } = body
if (!content && type !== 'checklist') {
return NextResponse.json(
{ success: false, error: 'Content is required' },
{ status: 400 }
)
}
const note = await prisma.note.create({
data: {
title: title || null,
content: content || '',
color: color || 'default',
type: type || 'text',
checkItems: checkItems ? JSON.stringify(checkItems) : null,
labels: labels ? JSON.stringify(labels) : null,
images: images ? JSON.stringify(images) : null,
}
})
return NextResponse.json({
success: true,
data: parseNote(note)
}, { status: 201 })
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to create note' },
{ status: 500 }
)
}
}
// PUT /api/notes - Update an existing note
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const { id, title, content, color, type, checkItems, labels, isPinned, isArchived, images } = body
if (!id) {
return NextResponse.json(
{ success: false, error: 'Note ID is required' },
{ status: 400 }
)
}
const updateData: any = {}
if (title !== undefined) updateData.title = title
if (content !== undefined) updateData.content = content
if (color !== undefined) updateData.color = color
if (type !== undefined) updateData.type = type
if (checkItems !== undefined) updateData.checkItems = checkItems ? JSON.stringify(checkItems) : null
if (labels !== undefined) updateData.labels = labels ? JSON.stringify(labels) : null
if (isPinned !== undefined) updateData.isPinned = isPinned
if (isArchived !== undefined) updateData.isArchived = isArchived
if (images !== undefined) updateData.images = images ? JSON.stringify(images) : null
const note = await prisma.note.update({
where: { id },
data: updateData
})
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to update note' },
{ status: 500 }
)
}
}
// DELETE /api/notes?id=xxx - Delete a note
export async function DELETE(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ success: false, error: 'Note ID is required' },
{ status: 400 }
)
}
await prisma.note.delete({
where: { id }
})
return NextResponse.json({
success: true,
message: 'Note deleted successfully'
})
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to delete note' },
{ status: 500 }
)
}
}