fix: improve note interactions and markdown LaTeX support
## Bug Fixes ### Note Card Actions - Fix broken size change functionality (missing state declaration) - Implement React 19 useOptimistic for instant UI feedback - Add startTransition for non-blocking updates - Ensure smooth animations without page refresh - All note actions now work: pin, archive, color, size, checklist ### Markdown LaTeX Rendering - Add remark-math and rehype-katex plugins - Support inline equations with dollar sign syntax - Support block equations with double dollar sign syntax - Import KaTeX CSS for proper styling - Equations now render correctly instead of showing raw LaTeX ## Technical Details - Replace undefined currentNote references with optimistic state - Add optimistic updates before server actions for instant feedback - Use router.refresh() in transitions for smart cache invalidation - Install remark-math, rehype-katex, and katex packages ## Testing - Build passes successfully with no TypeScript errors - Dev server hot-reloads changes correctly
This commit is contained in:
459
docs/data-models.md
Normal file
459
docs/data-models.md
Normal file
@@ -0,0 +1,459 @@
|
||||
# Data Models - Memento Project
|
||||
|
||||
## Overview
|
||||
|
||||
Memento uses SQLite as its database with Prisma ORM. The database schema is shared between the web application (keep-notes) and the MCP server.
|
||||
|
||||
**Database:** SQLite (`prisma/dev.db`)
|
||||
**ORM:** Prisma 5.22.0
|
||||
**Adapters:**
|
||||
- `@prisma/adapter-better-sqlite3` (primary)
|
||||
- `@prisma/adapter-libsql` (alternative for Turso)
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### User
|
||||
|
||||
Represents a user account with authentication and profile information.
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
password String? // Hashed password (bcrypt)
|
||||
role String @default("USER") // "USER" or "ADMIN"
|
||||
image String? // Profile picture URL
|
||||
theme String @default("light") // UI theme preference
|
||||
resetToken String? @unique // Password reset token
|
||||
resetTokenExpiry DateTime? // Reset token expiration
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
notes Note[]
|
||||
labels Label[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
**Relationships:**
|
||||
- One-to-many with Account (OAuth providers)
|
||||
- One-to-many with Session (active sessions)
|
||||
- One-to-many with Note (user's notes)
|
||||
- One-to-many with Label (user's labels)
|
||||
|
||||
**Fields:**
|
||||
- `id`: CUID (Collision-resistant Unique Identifier)
|
||||
- `email`: Unique email address
|
||||
- `password`: Optional (can be OAuth-only users)
|
||||
- `role`: RBAC - "USER" or "ADMIN"
|
||||
- `theme`: UI theme preference ("light" or "dark")
|
||||
- `resetToken`: Password recovery token (unique)
|
||||
- `resetTokenExpiry`: Token validity period
|
||||
|
||||
---
|
||||
|
||||
### Account
|
||||
|
||||
Stores OAuth provider account information (NextAuth.js).
|
||||
|
||||
```prisma
|
||||
model Account {
|
||||
userId String
|
||||
type String
|
||||
provider String // google, github, etc.
|
||||
providerAccountId String
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String?
|
||||
session_state String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([provider, providerAccountId])
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Links OAuth provider accounts to local User accounts
|
||||
|
||||
**Relationships:**
|
||||
- Many-to-one with User (via `userId`)
|
||||
|
||||
**Constraints:**
|
||||
- Composite unique key on `provider` + `providerAccountId`
|
||||
|
||||
---
|
||||
|
||||
### Session
|
||||
|
||||
Stores active user sessions (NextAuth.js).
|
||||
|
||||
```prisma
|
||||
model Session {
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Manages active user sessions for authentication
|
||||
|
||||
**Relationships:**
|
||||
- Many-to-one with User (via `userId`)
|
||||
|
||||
**Fields:**
|
||||
- `sessionToken`: Unique session identifier
|
||||
- `expires`: Session expiration timestamp
|
||||
|
||||
---
|
||||
|
||||
### VerificationToken
|
||||
|
||||
Stores email verification tokens (NextAuth.js).
|
||||
|
||||
```prisma
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String
|
||||
expires DateTime
|
||||
|
||||
@@id([identifier, token])
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Email verification flow
|
||||
|
||||
**Constraints:**
|
||||
- Composite unique key on `identifier` + `token`
|
||||
|
||||
---
|
||||
|
||||
### Label
|
||||
|
||||
User-defined labels/tags for organizing notes.
|
||||
|
||||
```prisma
|
||||
model Label {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
color String @default("gray")
|
||||
userId String? // Made optional for migration
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([name, userId]) // Labels unique per user
|
||||
@@index([userId])
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Categorization and organization of notes
|
||||
|
||||
**Relationships:**
|
||||
- Many-to-one with User (via `userId`)
|
||||
|
||||
**Fields:**
|
||||
- `name`: Label display name
|
||||
- `color`: Visual color (red, orange, yellow, green, teal, blue, purple, pink, gray)
|
||||
- `userId`: Optional (migration artifact, logic enforces ownership)
|
||||
|
||||
**Constraints:**
|
||||
- Unique label name per user
|
||||
- Indexed on `userId` for fast lookup
|
||||
|
||||
**Usage:**
|
||||
- Stored as JSON array in `Note.labels`
|
||||
- User can have multiple labels with the same name? No, unique constraint
|
||||
|
||||
---
|
||||
|
||||
### Note
|
||||
|
||||
Core data model - represents a note in the system.
|
||||
|
||||
```prisma
|
||||
model Note {
|
||||
id String @id @default(cuid())
|
||||
title String? // Optional title
|
||||
content String
|
||||
color String @default("default")
|
||||
isPinned Boolean @default(false)
|
||||
isArchived Boolean @default(false)
|
||||
type String @default("text") // "text" or "checklist"
|
||||
checkItems String? // JSON array
|
||||
labels String? // JSON array
|
||||
images String? // JSON array
|
||||
links String? // JSON array
|
||||
reminder DateTime? // Reminder timestamp
|
||||
isReminderDone Boolean @default(false)
|
||||
reminderRecurrence String? // "none", "daily", "weekly", "monthly", "custom"
|
||||
reminderLocation String? // Location-based reminders
|
||||
isMarkdown Boolean @default(false) // Markdown rendering
|
||||
size String @default("small") // "small", "medium", "large"
|
||||
embedding String? // Vector embeddings (JSON)
|
||||
userId String? // Owner
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([isPinned])
|
||||
@@index([isArchived])
|
||||
@@index([order])
|
||||
@@index([reminder])
|
||||
@@index([userId])
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Core note-taking entity
|
||||
|
||||
**Relationships:**
|
||||
- Many-to-one with User (via `userId`)
|
||||
|
||||
**Fields:**
|
||||
|
||||
**Basic:**
|
||||
- `id`: CUID
|
||||
- `title`: Optional heading/title
|
||||
- `content`: Note content (text or markdown)
|
||||
- `color`: Background color (default, red, orange, yellow, green, teal, blue, purple, pink, gray)
|
||||
- `type`: Note type - "text" or "checklist"
|
||||
|
||||
**State Flags:**
|
||||
- `isPinned`: Shows at top of list
|
||||
- `isArchived**: Removed from main view
|
||||
- `isReminderDone`: Reminder completed
|
||||
|
||||
**Rich Features:**
|
||||
- `checkItems`: JSON array of checklist items (if type=checklist)
|
||||
```json
|
||||
[
|
||||
{"id": "string", "text": "string", "checked": boolean}
|
||||
]
|
||||
```
|
||||
- `labels`: JSON array of label names
|
||||
```json
|
||||
["work", "ideas", "urgent"]
|
||||
```
|
||||
- `images`: JSON array of image URLs or base64
|
||||
```json
|
||||
["data:image/png;base64,..."]
|
||||
```
|
||||
- `links`: JSON array of link metadata
|
||||
- `reminder`: ISO8601 datetime for reminder
|
||||
- `reminderRecurrence`: Recurrence pattern
|
||||
- `reminderLocation`: Location-based reminders (future)
|
||||
- `isMarkdown`: Enable markdown rendering
|
||||
- `size`: Visual size (small, medium, large)
|
||||
- `embedding`: Vector embeddings for semantic search (JSON)
|
||||
|
||||
**Ordering:**
|
||||
- `order`: Manual sort order (drag-and-drop)
|
||||
|
||||
**Indexes:**
|
||||
- `isPinned`: Fast lookup for pinned notes
|
||||
- `isArchived`: Filter archived notes
|
||||
- `order`: Sort by manual ordering
|
||||
- `reminder`: Reminder queries (cron jobs)
|
||||
- `userId`: Filter by user
|
||||
|
||||
---
|
||||
|
||||
### SystemConfig
|
||||
|
||||
Key-value storage for system-wide configuration.
|
||||
|
||||
```prisma
|
||||
model SystemConfig {
|
||||
key String @id
|
||||
value String
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** System configuration and feature flags
|
||||
|
||||
**Examples:**
|
||||
- Feature toggles
|
||||
- System-wide settings
|
||||
- Admin configuration
|
||||
|
||||
---
|
||||
|
||||
## Relationships Diagram
|
||||
|
||||
```
|
||||
User (1) ----< (N) Account
|
||||
|
|
||||
| (1)
|
||||
|
|
||||
+----< (N) Session
|
||||
|
|
||||
| (1)
|
||||
|
|
||||
+----< (N) Note
|
||||
|
|
||||
| (1)
|
||||
|
|
||||
+----< (N) Label
|
||||
|
||||
VerificationToken (standalone)
|
||||
|
||||
SystemConfig (standalone)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSON Field Structures
|
||||
|
||||
### checkItems (Note)
|
||||
Array of checklist items:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "unique-id",
|
||||
"text": "Task description",
|
||||
"checked": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### labels (Note)
|
||||
Array of label names (strings):
|
||||
```json
|
||||
[
|
||||
"work",
|
||||
"ideas",
|
||||
"urgent"
|
||||
]
|
||||
```
|
||||
|
||||
### images (Note)
|
||||
Array of image URLs or base64 strings:
|
||||
```json
|
||||
[
|
||||
"data:image/png;base64,iVBORw0KGgo...",
|
||||
"/uploads/images/note-image.png"
|
||||
]
|
||||
```
|
||||
|
||||
### links (Note)
|
||||
Array of link metadata (structure TBD):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"title": "Example Site",
|
||||
"description": "..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### embedding (Note)
|
||||
Vector embeddings for semantic search (JSON string):
|
||||
```json
|
||||
[0.123, -0.456, 0.789, ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Operations
|
||||
|
||||
### Common Queries
|
||||
|
||||
**Get active user's notes:**
|
||||
```typescript
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isArchived: false
|
||||
},
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ order: 'asc' },
|
||||
{ updatedAt: 'desc' }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**Search notes:**
|
||||
```typescript
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ content: { contains: query, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Get user's labels:**
|
||||
```typescript
|
||||
const labels = await prisma.label.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Integrity
|
||||
|
||||
**Cascade Deletes:**
|
||||
- When User is deleted: delete their Accounts, Sessions, Notes, and Labels
|
||||
- Maintains referential integrity
|
||||
|
||||
**Unique Constraints:**
|
||||
- User.email
|
||||
- Account.provider + Account.providerAccountId
|
||||
- VerificationToken.identifier + VerificationToken.token
|
||||
- Label.name + Label.userId (per user)
|
||||
|
||||
**Indexes:**
|
||||
- All foreign keys indexed
|
||||
- Frequently queried fields indexed (isPinned, isArchived, order, reminder, userId)
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
**Current State:**
|
||||
- SQLite database for local development
|
||||
- Better-sqlite3 adapter for embedded usage
|
||||
- Optional LibSQL adapter for cloud deployment (Turso)
|
||||
|
||||
**Schema Evolution:**
|
||||
- `Label.userId` made optional for migration (logic enforces ownership)
|
||||
- JSON fields stored as strings (parsed in application layer)
|
||||
- Future: Consider PostgreSQL for production with proper JSONB support
|
||||
|
||||
---
|
||||
|
||||
## Database Size Considerations
|
||||
|
||||
**SQLite Limits:**
|
||||
- Max database size: 281 TB (theoretical)
|
||||
- Max row size: 1 GB
|
||||
- Max string/blob size: 1 GB
|
||||
|
||||
**Practical Considerations:**
|
||||
- Base64 images in Note.images could bloat database
|
||||
- Consider storing large files in filesystem and storing paths
|
||||
- Vector embeddings (Note.embedding) will grow with data
|
||||
|
||||
**Recommendations:**
|
||||
- Use CDN or object storage for images in production
|
||||
- Implement image compression before storage
|
||||
- Monitor database size with large note collections
|
||||
Reference in New Issue
Block a user