Add safe database migration workflow and note history infrastructure.
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
This introduces guarded migrations with automatic backups, fixes note creation after DB reset, and wires snapshot/restore history across notes surfaces.
This commit is contained in:
121
Momento-main/momento/memento-note/lib/note-history.ts
Normal file
121
Momento-main/momento/memento-note/lib/note-history.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { asArray } from '@/lib/utils'
|
||||
import type { NoteHistoryEntry } from '@/lib/types'
|
||||
|
||||
const HISTORY_TRACKED_FIELDS = [
|
||||
'title',
|
||||
'content',
|
||||
'color',
|
||||
'isPinned',
|
||||
'isArchived',
|
||||
'type',
|
||||
'checkItems',
|
||||
'labels',
|
||||
'images',
|
||||
'links',
|
||||
'isMarkdown',
|
||||
'size',
|
||||
'notebookId',
|
||||
] as const
|
||||
|
||||
export function shouldCaptureHistorySnapshot(data: Record<string, unknown>): boolean {
|
||||
return HISTORY_TRACKED_FIELDS.some((field) => field in data)
|
||||
}
|
||||
|
||||
export async function isNoteHistoryEnabledForUser(userId: string): Promise<boolean> {
|
||||
const settings = await prisma.userAISettings.findUnique({
|
||||
where: { userId },
|
||||
select: { noteHistory: true },
|
||||
})
|
||||
|
||||
return settings?.noteHistory === true
|
||||
}
|
||||
|
||||
export async function createNoteHistorySnapshot({
|
||||
noteId,
|
||||
userId,
|
||||
reason,
|
||||
}: {
|
||||
noteId: string
|
||||
userId: string
|
||||
reason?: string
|
||||
}): Promise<void> {
|
||||
const note = await prisma.note.findFirst({
|
||||
where: { id: noteId, userId },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
title: true,
|
||||
content: true,
|
||||
color: true,
|
||||
isPinned: true,
|
||||
isArchived: true,
|
||||
type: true,
|
||||
checkItems: true,
|
||||
labels: true,
|
||||
images: true,
|
||||
links: true,
|
||||
isMarkdown: true,
|
||||
size: true,
|
||||
notebookId: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!note || !note.userId) return
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const lastVersionEntry = await (tx as any).noteHistory.findFirst({
|
||||
where: { noteId },
|
||||
orderBy: { version: 'desc' },
|
||||
select: { version: true },
|
||||
})
|
||||
|
||||
const nextVersion = ((lastVersionEntry?.version as number | undefined) ?? 0) + 1
|
||||
|
||||
await (tx as any).noteHistory.create({
|
||||
data: {
|
||||
noteId: note.id,
|
||||
userId: note.userId,
|
||||
version: nextVersion,
|
||||
reason: reason ?? null,
|
||||
title: note.title,
|
||||
content: note.content,
|
||||
color: note.color,
|
||||
isPinned: note.isPinned,
|
||||
isArchived: note.isArchived,
|
||||
type: note.type,
|
||||
checkItems: note.checkItems,
|
||||
labels: note.labels,
|
||||
images: note.images,
|
||||
links: note.links,
|
||||
isMarkdown: note.isMarkdown,
|
||||
size: note.size,
|
||||
notebookId: note.notebookId,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function parseNoteHistoryEntry(entry: any): NoteHistoryEntry {
|
||||
return {
|
||||
id: entry.id,
|
||||
noteId: entry.noteId,
|
||||
userId: entry.userId,
|
||||
version: entry.version,
|
||||
reason: entry.reason ?? null,
|
||||
title: entry.title ?? null,
|
||||
content: entry.content,
|
||||
color: entry.color,
|
||||
isPinned: entry.isPinned,
|
||||
isArchived: entry.isArchived,
|
||||
type: entry.type,
|
||||
checkItems: asArray(entry.checkItems, null as any) ?? null,
|
||||
labels: asArray(entry.labels) || null,
|
||||
images: asArray(entry.images) || null,
|
||||
links: asArray(entry.links) || null,
|
||||
isMarkdown: entry.isMarkdown,
|
||||
size: entry.size,
|
||||
notebookId: entry.notebookId ?? null,
|
||||
createdAt: entry.createdAt,
|
||||
}
|
||||
}
|
||||
232
Momento-main/momento/memento-note/lib/types.ts
Normal file
232
Momento-main/momento/memento-note/lib/types.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
export interface CheckItem {
|
||||
id: string;
|
||||
text: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notebook model for organizing notes
|
||||
*/
|
||||
export interface Notebook {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
color: string | null;
|
||||
order: number;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// Relations
|
||||
notes?: Note[];
|
||||
labels?: Label[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Label model - contextual to notebooks
|
||||
*/
|
||||
export interface Label {
|
||||
id: string;
|
||||
name: string;
|
||||
color: LabelColorName;
|
||||
notebookId: string | null; // NEW: Belongs to a notebook
|
||||
userId?: string | null; // DEPRECATED: Kept for backward compatibility
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// Relations
|
||||
notebook?: Notebook | null;
|
||||
}
|
||||
|
||||
export interface LinkMetadata {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
imageUrl?: string;
|
||||
siteName?: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: string;
|
||||
title: string | null;
|
||||
content: string;
|
||||
color: string;
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
trashedAt?: Date | null;
|
||||
type: 'text' | 'checklist';
|
||||
checkItems: CheckItem[] | null;
|
||||
labels: string[] | null; // DEPRECATED: Array of label names stored as JSON string
|
||||
images: string[] | null;
|
||||
links: LinkMetadata[] | null;
|
||||
reminder: Date | null;
|
||||
isReminderDone: boolean;
|
||||
reminderRecurrence: string | null;
|
||||
reminderLocation: string | null;
|
||||
isMarkdown: boolean;
|
||||
dismissedFromRecent?: boolean;
|
||||
size: NoteSize;
|
||||
order: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
contentUpdatedAt: Date;
|
||||
sharedWith?: string[];
|
||||
userId?: string | null;
|
||||
// Notebook relation (optional - null = "General Notes" / Inbox)
|
||||
notebookId?: string | null;
|
||||
notebook?: Notebook | null;
|
||||
autoGenerated?: boolean | null;
|
||||
aiProvider?: string | null;
|
||||
// Search result metadata (optional)
|
||||
matchType?: 'exact' | 'related' | null;
|
||||
searchScore?: number | null;
|
||||
}
|
||||
|
||||
export interface NoteHistoryEntry {
|
||||
id: string;
|
||||
noteId: string;
|
||||
userId: string;
|
||||
version: number;
|
||||
reason: string | null;
|
||||
title: string | null;
|
||||
content: string;
|
||||
color: string;
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
type: 'text' | 'checklist';
|
||||
checkItems: CheckItem[] | null;
|
||||
labels: string[] | null;
|
||||
images: string[] | null;
|
||||
links: LinkMetadata[] | null;
|
||||
isMarkdown: boolean;
|
||||
size: NoteSize;
|
||||
notebookId: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export type NoteSize = 'small' | 'medium' | 'large';
|
||||
|
||||
export interface LabelWithColor {
|
||||
name: string;
|
||||
color: LabelColorName;
|
||||
}
|
||||
|
||||
export const LABEL_COLORS = {
|
||||
gray: {
|
||||
bg: 'bg-zinc-100 dark:bg-zinc-800',
|
||||
text: 'text-zinc-700 dark:text-zinc-300',
|
||||
border: 'border-zinc-200 dark:border-zinc-700',
|
||||
icon: 'text-zinc-500 dark:text-zinc-400'
|
||||
},
|
||||
red: {
|
||||
bg: 'bg-rose-100 dark:bg-rose-900/40',
|
||||
text: 'text-rose-700 dark:text-rose-300',
|
||||
border: 'border-rose-200 dark:border-rose-800',
|
||||
icon: 'text-rose-500 dark:text-rose-400'
|
||||
},
|
||||
orange: {
|
||||
bg: 'bg-orange-100 dark:bg-orange-900/40',
|
||||
text: 'text-orange-700 dark:text-orange-300',
|
||||
border: 'border-orange-200 dark:border-orange-800',
|
||||
icon: 'text-orange-500 dark:text-orange-400'
|
||||
},
|
||||
yellow: {
|
||||
bg: 'bg-amber-100 dark:bg-amber-900/40',
|
||||
text: 'text-amber-700 dark:text-amber-300',
|
||||
border: 'border-amber-200 dark:border-amber-800',
|
||||
icon: 'text-amber-500 dark:text-amber-400'
|
||||
},
|
||||
green: {
|
||||
bg: 'bg-emerald-100 dark:bg-emerald-900/40',
|
||||
text: 'text-emerald-700 dark:text-emerald-300',
|
||||
border: 'border-emerald-200 dark:border-emerald-800',
|
||||
icon: 'text-emerald-500 dark:text-emerald-400'
|
||||
},
|
||||
teal: {
|
||||
bg: 'bg-teal-100 dark:bg-teal-900/40',
|
||||
text: 'text-teal-700 dark:text-teal-300',
|
||||
border: 'border-teal-200 dark:border-teal-800',
|
||||
icon: 'text-teal-500 dark:text-teal-400'
|
||||
},
|
||||
blue: {
|
||||
bg: 'bg-sky-100 dark:bg-sky-900/40',
|
||||
text: 'text-sky-700 dark:text-sky-300',
|
||||
border: 'border-sky-200 dark:border-sky-800',
|
||||
icon: 'text-sky-500 dark:text-sky-400'
|
||||
},
|
||||
purple: {
|
||||
bg: 'bg-violet-100 dark:bg-violet-900/40',
|
||||
text: 'text-violet-700 dark:text-violet-300',
|
||||
border: 'border-violet-200 dark:border-violet-800',
|
||||
icon: 'text-violet-500 dark:text-violet-400'
|
||||
},
|
||||
pink: {
|
||||
bg: 'bg-fuchsia-100 dark:bg-fuchsia-900/40',
|
||||
text: 'text-fuchsia-700 dark:text-fuchsia-300',
|
||||
border: 'border-fuchsia-200 dark:border-fuchsia-800',
|
||||
icon: 'text-fuchsia-500 dark:text-fuchsia-400'
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type LabelColorName = keyof typeof LABEL_COLORS
|
||||
|
||||
export const NOTE_COLORS = {
|
||||
default: {
|
||||
bg: 'bg-white dark:bg-zinc-900',
|
||||
hover: 'hover:bg-gray-50 dark:hover:bg-zinc-800',
|
||||
card: 'bg-white dark:bg-zinc-900 border-gray-200 dark:border-zinc-700'
|
||||
},
|
||||
red: {
|
||||
bg: 'bg-red-50 dark:bg-red-950/30',
|
||||
hover: 'hover:bg-red-100 dark:hover:bg-red-950/50',
|
||||
card: 'bg-red-50 dark:bg-red-950/30 border-red-100 dark:border-red-900/50'
|
||||
},
|
||||
orange: {
|
||||
bg: 'bg-orange-50 dark:bg-orange-950/30',
|
||||
hover: 'hover:bg-orange-100 dark:hover:bg-orange-950/50',
|
||||
card: 'bg-orange-50 dark:bg-orange-950/30 border-orange-100 dark:border-orange-900/50'
|
||||
},
|
||||
yellow: {
|
||||
bg: 'bg-yellow-50 dark:bg-yellow-950/30',
|
||||
hover: 'hover:bg-yellow-100 dark:hover:bg-yellow-950/50',
|
||||
card: 'bg-yellow-50 dark:bg-yellow-950/30 border-yellow-100 dark:border-yellow-900/50'
|
||||
},
|
||||
green: {
|
||||
bg: 'bg-green-50 dark:bg-green-950/30',
|
||||
hover: 'hover:bg-green-100 dark:hover:bg-green-950/50',
|
||||
card: 'bg-green-50 dark:bg-green-950/30 border-green-100 dark:border-green-900/50'
|
||||
},
|
||||
teal: {
|
||||
bg: 'bg-teal-50 dark:bg-teal-950/30',
|
||||
hover: 'hover:bg-teal-100 dark:hover:bg-teal-950/50',
|
||||
card: 'bg-teal-50 dark:bg-teal-950/30 border-teal-100 dark:border-teal-900/50'
|
||||
},
|
||||
blue: {
|
||||
bg: 'bg-blue-50 dark:bg-blue-950/30',
|
||||
hover: 'hover:bg-blue-100 dark:hover:bg-blue-950/50',
|
||||
card: 'bg-blue-50 dark:bg-blue-950/30 border-blue-100 dark:border-blue-900/50'
|
||||
},
|
||||
purple: {
|
||||
bg: 'bg-purple-50 dark:bg-purple-950/30',
|
||||
hover: 'hover:bg-purple-100 dark:hover:bg-purple-950/50',
|
||||
card: 'bg-purple-50 dark:bg-purple-950/30 border-purple-100 dark:border-purple-900/50'
|
||||
},
|
||||
pink: {
|
||||
bg: 'bg-pink-50 dark:bg-pink-950/30',
|
||||
hover: 'hover:bg-pink-100 dark:hover:bg-pink-950/50',
|
||||
card: 'bg-pink-50 dark:bg-pink-950/30 border-pink-100 dark:border-pink-900/50'
|
||||
},
|
||||
gray: {
|
||||
bg: 'bg-gray-100 dark:bg-gray-800/50',
|
||||
hover: 'hover:bg-gray-200 dark:hover:bg-gray-700/50',
|
||||
card: 'bg-gray-100 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type NoteColor = keyof typeof NOTE_COLORS;
|
||||
|
||||
/**
|
||||
* Query types for adaptive search weighting
|
||||
* - 'exact': User searched with quotes, looking for exact match (e.g., "Error 404")
|
||||
* - 'conceptual': User is asking a question or looking for concepts (e.g., "how to cook")
|
||||
* - 'mixed': No specific pattern detected, use default weights
|
||||
*/
|
||||
export type QueryType = 'exact' | 'conceptual' | 'mixed';
|
||||
Reference in New Issue
Block a user