Files
Momento/memento-note/app/api/notebooks/[id]/schema/route.ts
Antigravity 0784c94242
Some checks failed
CI / Lint, Test & Build (push) Failing after 57s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note)
avec activation guidée, tableau éditable, kanban et suppression de colonnes.
Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN.
Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la
robustesse du serveur MCP (config, validation, rate-limit, métriques).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:03:16 +00:00

205 lines
7.3 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { MAX_PROPERTIES_PER_NOTEBOOK } from '@/lib/structured-views/types'
import { isValidPropertyType } from '@/lib/structured-views/property-utils'
import { buildNoteValuesMap, parseViewSettings, serializeSchema } from '@/lib/structured-views/schema-serialize'
async function assertNotebookAccess(notebookId: string, userId: string) {
return prisma.notebook.findFirst({
where: { id: notebookId, userId, trashedAt: null },
select: { id: true },
})
}
const schemaInclude = {
properties: { orderBy: { position: 'asc' as const } },
} as const
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: notebookId } = await params
const notebook = await assertNotebookAccess(notebookId, session.user.id)
if (!notebook) {
return NextResponse.json({ success: false, error: 'Notebook not found' }, { status: 404 })
}
const raw = await prisma.notebookSchema.findUnique({
where: { notebookId },
include: schemaInclude,
})
const schema = serializeSchema(raw)
if (!schema) {
return NextResponse.json({ success: true, data: { schema: null, noteValues: {} } })
}
const notes = await prisma.note.findMany({
where: { notebookId, userId: session.user.id, trashedAt: null },
select: { id: true },
})
const noteIds = notes.map((n) => n.id)
const rows = noteIds.length
? await prisma.noteProperty.findMany({
where: { noteId: { in: noteIds } },
include: { property: { select: { type: true } } },
})
: []
const noteValues = buildNoteValuesMap(noteIds, rows)
return NextResponse.json({ success: true, data: { schema, noteValues } })
} catch (error) {
console.error('GET notebook schema:', error)
return NextResponse.json({ success: false, error: 'Failed to load schema' }, { status: 500 })
}
}
export async function POST(
_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: notebookId } = await params
const notebook = await assertNotebookAccess(notebookId, session.user.id)
if (!notebook) {
return NextResponse.json({ success: false, error: 'Notebook not found' }, { status: 404 })
}
const existing = await prisma.notebookSchema.findUnique({ where: { notebookId } })
if (existing) {
const raw = await prisma.notebookSchema.findUnique({
where: { notebookId },
include: schemaInclude,
})
return NextResponse.json({ success: true, data: { schema: serializeSchema(raw) } })
}
const created = await prisma.notebookSchema.create({
data: { notebookId },
include: schemaInclude,
})
return NextResponse.json({ success: true, data: { schema: serializeSchema(created) } })
} catch (error) {
console.error('POST notebook schema:', error)
return NextResponse.json({ success: false, error: 'Failed to create schema' }, { status: 500 })
}
}
export async function PATCH(
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: notebookId } = await params
const notebook = await assertNotebookAccess(notebookId, session.user.id)
if (!notebook) {
return NextResponse.json({ success: false, error: 'Notebook not found' }, { status: 404 })
}
const body = await request.json()
const action = typeof body.action === 'string' ? body.action : ''
let schema = await prisma.notebookSchema.findUnique({ where: { notebookId } })
if (!schema) {
schema = await prisma.notebookSchema.create({ data: { notebookId } })
}
if (action === 'addProperty') {
const name = typeof body.name === 'string' ? body.name.trim() : ''
const type = typeof body.type === 'string' ? body.type : ''
if (!name || !isValidPropertyType(type)) {
return NextResponse.json({ success: false, error: 'Invalid property' }, { status: 400 })
}
const count = await prisma.notebookProperty.count({ where: { schemaId: schema.id } })
if (count >= MAX_PROPERTIES_PER_NOTEBOOK) {
return NextResponse.json({ success: false, error: 'Max properties reached' }, { status: 400 })
}
let options: string[] = []
if (type === 'select' || type === 'multiselect') {
options = Array.isArray(body.options)
? body.options
.filter((o: unknown): o is string => typeof o === 'string' && o.trim().length > 0)
.map((o: string) => o.trim())
: []
if (options.length === 0) {
return NextResponse.json({ success: false, error: 'Options required' }, { status: 400 })
}
}
await prisma.notebookProperty.create({
data: {
schemaId: schema.id,
name,
type,
options: options.length ? JSON.stringify(options) : null,
position: count,
},
})
} else if (action === 'deleteProperty') {
const propertyId = typeof body.propertyId === 'string' ? body.propertyId : ''
if (!propertyId) {
return NextResponse.json({ success: false, error: 'propertyId required' }, { status: 400 })
}
const currentSettings = parseViewSettings(schema.viewSettings)
await prisma.notebookProperty.deleteMany({
where: { id: propertyId, schemaId: schema.id },
})
if (currentSettings.kanbanGroupPropertyId === propertyId) {
await prisma.notebookSchema.update({
where: { id: schema.id },
data: { viewSettings: JSON.stringify({ ...currentSettings, kanbanGroupPropertyId: null }) },
})
}
} else if (action === 'updateViewSettings') {
const current = parseViewSettings(schema.viewSettings)
const next: Record<string, unknown> = { ...current }
if ('kanbanGroupPropertyId' in body) {
next.kanbanGroupPropertyId =
typeof body.kanbanGroupPropertyId === 'string' ? body.kanbanGroupPropertyId : null
}
await prisma.notebookSchema.update({
where: { id: schema.id },
data: { viewSettings: JSON.stringify(next) },
})
} else if (action === 'disable') {
await prisma.notebookSchema.delete({ where: { id: schema.id } })
return NextResponse.json({ success: true, data: { schema: null } })
} else {
return NextResponse.json({ success: false, error: 'Unknown action' }, { status: 400 })
}
const raw = await prisma.notebookSchema.findUnique({
where: { notebookId },
include: schemaInclude,
})
return NextResponse.json({ success: true, data: { schema: serializeSchema(raw) } })
} catch (error) {
console.error('PATCH notebook schema:', error)
return NextResponse.json({ success: false, error: 'Failed to update schema' }, { status: 500 })
}
}