diff --git a/k-notes-frontend/src/components/note-form.tsx b/k-notes-frontend/src/components/note-form.tsx index 2069eea..dc171b8 100644 --- a/k-notes-frontend/src/components/note-form.tsx +++ b/k-notes-frontend/src/components/note-form.tsx @@ -11,7 +11,7 @@ import { Editor } from "@/components/editor/editor"; import { useTranslation } from "react-i18next"; const noteSchema = (t: any) => z.object({ - title: z.string().min(1, t("Title is required")).max(200, t("Title too long")), + title: z.string().min(0, t("Title too long")).max(200, t("Title too long")), content: z.string().optional(), is_pinned: z.boolean().default(false), tags: z.string().optional(), // Comma separated for now diff --git a/migrations/20251231000000_nullable_title.sql b/migrations/20251231000000_nullable_title.sql new file mode 100644 index 0000000..c7329a3 --- /dev/null +++ b/migrations/20251231000000_nullable_title.sql @@ -0,0 +1,45 @@ +-- Allow NULL titles in notes table +-- SQLite doesn't support ALTER COLUMN, so we need to recreate the table + +-- Step 1: Create new table with nullable title +CREATE TABLE notes_new ( + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title TEXT, -- Now nullable + content TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT 'DEFAULT', + is_pinned INTEGER NOT NULL DEFAULT 0, + is_archived INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Step 2: Copy data from old table +INSERT INTO notes_new (id, user_id, title, content, color, is_pinned, is_archived, created_at, updated_at) +SELECT id, user_id, title, content, color, is_pinned, is_archived, created_at, updated_at FROM notes; + +-- Step 3: Drop old table +DROP TABLE notes; + +-- Step 4: Rename new table +ALTER TABLE notes_new RENAME TO notes; + +-- Step 5: Recreate indexes +CREATE INDEX idx_notes_user_id ON notes(user_id); +CREATE INDEX idx_notes_is_pinned ON notes(is_pinned); +CREATE INDEX idx_notes_is_archived ON notes(is_archived); +CREATE INDEX idx_notes_updated_at ON notes(updated_at); + +-- Step 6: Recreate FTS triggers +CREATE TRIGGER notes_ai AFTER INSERT ON notes BEGIN + INSERT INTO notes_fts(rowid, title, content) VALUES (NEW.rowid, COALESCE(NEW.title, ''), NEW.content); +END; + +CREATE TRIGGER notes_ad AFTER DELETE ON notes BEGIN + INSERT INTO notes_fts(notes_fts, rowid, title, content) VALUES('delete', OLD.rowid, COALESCE(OLD.title, ''), OLD.content); +END; + +CREATE TRIGGER notes_au AFTER UPDATE ON notes BEGIN + INSERT INTO notes_fts(notes_fts, rowid, title, content) VALUES('delete', OLD.rowid, COALESCE(OLD.title, ''), OLD.content); + INSERT INTO notes_fts(rowid, title, content) VALUES (NEW.rowid, COALESCE(NEW.title, ''), NEW.content); +END; diff --git a/notes-api/src/dto.rs b/notes-api/src/dto.rs index 5dd4200..46d9fbd 100644 --- a/notes-api/src/dto.rs +++ b/notes-api/src/dto.rs @@ -10,7 +10,7 @@ use notes_domain::{Note, Tag}; /// Request to create a new note #[derive(Debug, Deserialize, Validate)] pub struct CreateNoteRequest { - #[validate(length(min = 1, max = 200, message = "Title must be 1-200 characters"))] + #[validate(length(max = 200, message = "Title must be at most 200 characters"))] pub title: String, #[serde(default)] @@ -29,7 +29,7 @@ pub struct CreateNoteRequest { /// Request to update an existing note (all fields optional) #[derive(Debug, Deserialize, Validate)] pub struct UpdateNoteRequest { - #[validate(length(min = 1, max = 200, message = "Title must be 1-200 characters"))] + #[validate(length(max = 200, message = "Title must be at most 200 characters"))] pub title: Option, pub content: Option,