import { clampNoteBody, createSeedState, normalizeStateForWrite } from "./state-utils.mjs"; export function createD1Repository({ db }) { return { runtime: "workerd", async init() { await db.exec(schemaSql); await seedDatabase(db); }, async readState() { return readState(db); }, async writeState(state) { await writeState(db, state); return readState(db); }, async createNote(note) { return createNote(db, note); }, async updateNote(id, patch) { return updateNote(db, id, patch); }, async deleteNote(id) { await db.prepare("DELETE FROM notes WHERE id = ?").bind(id).run(); }, async createFolder(folder) { return createFolder(db, folder); }, async updateFolder(id, patch) { return updateFolder(db, id, patch); }, async deleteFolder(id) { return deleteFolder(db, id); }, async getUser() { return await db.prepare("SELECT id, password_hash AS passwordHash, created_at AS createdAt FROM users ORDER BY created_at ASC LIMIT 1").first() || null; }, async createUser({ passwordHash }) { const now = Date.now(); const id = crypto.randomUUID(); await db.prepare("INSERT INTO users (id, password_hash, created_at) VALUES (?, ?, ?)").bind(id, passwordHash, now).run(); return { id, passwordHash, createdAt: now }; }, async updateUserPassword({ passwordHash }) { const user = await this.getUser(); if (!user) return null; await db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").bind(passwordHash, user.id).run(); return { ...user, passwordHash }; }, async createSession({ tokenHash, expiresAt }) { const id = crypto.randomUUID(); await db.prepare("INSERT INTO sessions (id, token_hash, expires_at, created_at) VALUES (?, ?, ?, ?)").bind(id, tokenHash, expiresAt, Date.now()).run(); return { id, tokenHash, expiresAt }; }, async getSession(tokenHash) { await db.prepare("DELETE FROM sessions WHERE expires_at <= ?").bind(Date.now()).run(); return await db.prepare("SELECT id, token_hash AS tokenHash, expires_at AS expiresAt FROM sessions WHERE token_hash = ? LIMIT 1").bind(tokenHash).first() || null; }, async deleteSession(tokenHash) { await db.prepare("DELETE FROM sessions WHERE token_hash = ?").bind(tokenHash).run(); }, async deleteAllSessions() { await db.prepare("DELETE FROM sessions").run(); } }; } const schemaSql = ` CREATE TABLE IF NOT EXISTS folders ( id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, version INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS notes ( id TEXT PRIMARY KEY, folder TEXT NOT NULL, body TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, version INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(folder) REFERENCES folders(id) ); CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, password_hash TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL ); `; async function seedDatabase(db) { const existing = await db.prepare("SELECT COUNT(*) AS count FROM folders").first(); if (existing?.count > 0) return; await writeState(db, createSeedState()); } async function readState(db) { const foldersResult = await db.prepare(` SELECT id, name, created_at AS createdAt, updated_at AS updatedAt, version FROM folders ORDER BY created_at ASC `).all(); const notesResult = await db.prepare(` SELECT id, folder, body, created_at AS createdAt, updated_at AS updatedAt, version FROM notes ORDER BY updated_at DESC `).all(); return { folders: foldersResult.results || [], notes: notesResult.results || [] }; } async function writeState(db, state) { const { folders, notes } = normalizeStateForWrite(state); const statements = [ db.prepare("DELETE FROM notes"), db.prepare("DELETE FROM folders"), ...folders.map((folder) => db.prepare(` INSERT INTO folders (id, name, created_at, updated_at, version) VALUES (?, ?, ?, ?, ?) `).bind(folder.id, folder.name, folder.createdAt, folder.updatedAt, folder.version)), ...notes.map((note) => db.prepare(` INSERT INTO notes (id, folder, body, created_at, updated_at, version) VALUES (?, ?, ?, ?, ?, ?) `).bind(note.id, note.folder, note.body, note.createdAt, note.updatedAt, note.version)) ]; await db.batch(statements); } async function createNote(db, note) { const now = Date.now(); const folder = await db.prepare("SELECT id FROM folders WHERE id = ? LIMIT 1").bind(note.folder).first(); const folderId = folder?.id || "notes"; const createdAt = Number(note.createdAt || now); const updatedAt = Number(note.updatedAt || now); const id = String(note.id || crypto.randomUUID()); await db.prepare(` INSERT INTO notes (id, folder, body, created_at, updated_at, version) VALUES (?, ?, ?, ?, ?, 1) `).bind(id, folderId, clampNoteBody(note.body), createdAt, updatedAt).run(); return getNote(db, id); } async function updateNote(db, id, patch) { const existing = await getNote(db, id); if (!existing) return { status: "missing" }; const expectedVersion = Number(patch.version); if (expectedVersion && expectedVersion !== existing.version) { return { status: "conflict", note: existing }; } const folder = patch.folder ? await db.prepare("SELECT id FROM folders WHERE id = ? LIMIT 1").bind(patch.folder).first() : null; const nextFolder = folder?.id || existing.folder; const nextBody = patch.body === undefined ? existing.body : clampNoteBody(patch.body); const nextUpdatedAt = Number(patch.updatedAt || Date.now()); const nextVersion = existing.version + 1; await db.prepare(` UPDATE notes SET folder = ?, body = ?, updated_at = ?, version = ? WHERE id = ? `).bind(nextFolder, nextBody, nextUpdatedAt, nextVersion, id).run(); return { status: "ok", note: await getNote(db, id) }; } async function getNote(db, id) { return await db.prepare(` SELECT id, folder, body, created_at AS createdAt, updated_at AS updatedAt, version FROM notes WHERE id = ? LIMIT 1 `).bind(id).first() || null; } async function createFolder(db, folder) { const now = Date.now(); const id = String(folder.id || crypto.randomUUID()); const name = String(folder.name || "未命名文件夹").trim() || "未命名文件夹"; const createdAt = Number(folder.createdAt || now); const updatedAt = Number(folder.updatedAt || now); await db.prepare(` INSERT INTO folders (id, name, created_at, updated_at, version) VALUES (?, ?, ?, ?, 1) `).bind(id, name, createdAt, updatedAt).run(); return getFolder(db, id); } async function updateFolder(db, id, patch) { const existing = await getFolder(db, id); if (!existing || existing.id === "notes") return { status: "missing" }; const expectedVersion = Number(patch.version); if (expectedVersion && expectedVersion !== existing.version) { return { status: "conflict", folder: existing }; } const nextName = String(patch.name ?? existing.name).trim() || existing.name; const nextUpdatedAt = Number(patch.updatedAt || Date.now()); const nextVersion = existing.version + 1; await db.prepare(` UPDATE folders SET name = ?, updated_at = ?, version = ? WHERE id = ? `).bind(nextName, nextUpdatedAt, nextVersion, id).run(); return { status: "ok", folder: await getFolder(db, id) }; } async function deleteFolder(db, id) { const folder = await getFolder(db, id); if (!folder || folder.id === "notes") return { status: "missing" }; const fallback = await getFolder(db, "notes") || await db.prepare("SELECT id FROM folders ORDER BY created_at ASC LIMIT 1").first(); if (!fallback) return { status: "missing" }; const now = Date.now(); await db.batch([ db.prepare("UPDATE notes SET folder = ?, updated_at = ?, version = version + 1 WHERE folder = ?").bind(fallback.id, now, folder.id), db.prepare("DELETE FROM folders WHERE id = ?").bind(folder.id) ]); return { status: "ok", fallbackFolderId: fallback.id }; } async function getFolder(db, id) { return await db.prepare(` SELECT id, name, created_at AS createdAt, updated_at AS updatedAt, version FROM folders WHERE id = ? LIMIT 1 `).bind(id).first() || null; }