mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-14 12:28:18 +02:00
fix: resolve N+1 queries, add indexes, and improve DB operations
- Batch-fetch tags for notes in a single query instead of per-note loops - Add composite indexes on notes, note_tags, and sync_log tables - Wrap sync push in a transaction for atomicity - Replace O(n) cleanExpiredSessions with single DELETE query - Use SQL WHERE in getChangesSince instead of loading all notes - Extract shared syncNoteTags utility to eliminate duplication - Add root +error.svelte boundary matching app theme Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
02e6b57764
commit
ef3ba163dd
@@ -1,10 +1,11 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { notes, noteTags, tags } from '$lib/server/db/schema.js';
|
||||
import { notes } from '$lib/server/db/schema.js';
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { extractTags } from '$lib/utils/tags.js';
|
||||
import { fetchTagsForNotes, syncNoteTags } from '$lib/server/tags.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const filter = url.searchParams.get('filter') || 'all';
|
||||
@@ -21,23 +22,18 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
conditions = and(eq(notes.archived, false), eq(notes.trashed, false));
|
||||
}
|
||||
|
||||
const result = await db
|
||||
const result = db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(conditions)
|
||||
.orderBy(desc(notes.pinned), desc(notes.updatedAt));
|
||||
.orderBy(desc(notes.pinned), desc(notes.updatedAt))
|
||||
.all();
|
||||
|
||||
// Fetch tags for each note
|
||||
const notesWithTags = await Promise.all(
|
||||
result.map(async (note) => {
|
||||
const tagRows = await db
|
||||
.select({ name: tags.name })
|
||||
.from(noteTags)
|
||||
.innerJoin(tags, eq(noteTags.tagId, tags.id))
|
||||
.where(eq(noteTags.noteId, note.id));
|
||||
return { ...note, tags: tagRows.map((t) => t.name) };
|
||||
})
|
||||
);
|
||||
const tagMap = fetchTagsForNotes(result.map((n) => n.id));
|
||||
const notesWithTags = result.map((note) => ({
|
||||
...note,
|
||||
tags: tagMap.get(note.id) ?? []
|
||||
}));
|
||||
|
||||
return json(notesWithTags);
|
||||
};
|
||||
@@ -63,27 +59,13 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
version: 1
|
||||
};
|
||||
|
||||
await db.insert(notes).values(newNote);
|
||||
|
||||
// Extract and save tags
|
||||
const content = `${newNote.title} ${newNote.content}`;
|
||||
const extractedTags = extractTags(content);
|
||||
await syncNoteTags(id, extractedTags);
|
||||
|
||||
db.transaction((tx) => {
|
||||
tx.insert(notes).values(newNote).run();
|
||||
});
|
||||
syncNoteTags(id, extractedTags);
|
||||
|
||||
return json({ ...newNote, tags: extractedTags }, { status: 201 });
|
||||
};
|
||||
|
||||
async function syncNoteTags(noteId: string, tagNames: string[]) {
|
||||
// Remove existing tag associations
|
||||
await db.delete(noteTags).where(eq(noteTags.noteId, noteId));
|
||||
|
||||
for (const name of tagNames) {
|
||||
// Upsert tag
|
||||
let tagRow = await db.select().from(tags).where(eq(tags.name, name)).get();
|
||||
if (!tagRow) {
|
||||
const result = await db.insert(tags).values({ name }).returning();
|
||||
tagRow = result[0];
|
||||
}
|
||||
await db.insert(noteTags).values({ noteId, tagId: tagRow.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { notes, noteTags, tags } from '$lib/server/db/schema.js';
|
||||
import { notes } from '$lib/server/db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { extractTags } from '$lib/utils/tags.js';
|
||||
import { fetchTagsForNotes, syncNoteTags } from '$lib/server/tags.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const note = await db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
const note = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
|
||||
if (!note) {
|
||||
throw error(404, 'Note not found');
|
||||
}
|
||||
|
||||
const tagRows = await db
|
||||
.select({ name: tags.name })
|
||||
.from(noteTags)
|
||||
.innerJoin(tags, eq(noteTags.tagId, tags.id))
|
||||
.where(eq(noteTags.noteId, note.id));
|
||||
const tagMap = fetchTagsForNotes([note.id]);
|
||||
|
||||
return json({ ...note, tags: tagRows.map((t) => t.name) });
|
||||
return json({ ...note, tags: tagMap.get(note.id) ?? [] });
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
const body = await request.json();
|
||||
const existing = await db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
const existing = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
|
||||
if (!existing) {
|
||||
throw error(404, 'Note not found');
|
||||
@@ -47,46 +44,28 @@ export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
if (body.checklistMode !== undefined) updates.checklistMode = body.checklistMode;
|
||||
if (body.sortOrder !== undefined) updates.sortOrder = body.sortOrder;
|
||||
|
||||
await db.update(notes).set(updates).where(eq(notes.id, params.id));
|
||||
db.update(notes).set(updates).where(eq(notes.id, params.id)).run();
|
||||
|
||||
const updated = await db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
const updated = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
|
||||
// Re-sync tags if content or title changed
|
||||
if (body.title !== undefined || body.content !== undefined) {
|
||||
const content = `${updated!.title} ${updated!.content}`;
|
||||
const extractedTags = extractTags(content);
|
||||
await syncNoteTags(params.id, extractedTags);
|
||||
syncNoteTags(params.id, extractedTags);
|
||||
}
|
||||
|
||||
const tagRows = await db
|
||||
.select({ name: tags.name })
|
||||
.from(noteTags)
|
||||
.innerJoin(tags, eq(noteTags.tagId, tags.id))
|
||||
.where(eq(noteTags.noteId, params.id));
|
||||
const tagMap = fetchTagsForNotes([params.id]);
|
||||
|
||||
return json({ ...updated, tags: tagRows.map((t) => t.name) });
|
||||
return json({ ...updated, tags: tagMap.get(params.id) ?? [] });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params }) => {
|
||||
const existing = await db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
const existing = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
|
||||
if (!existing) {
|
||||
throw error(404, 'Note not found');
|
||||
}
|
||||
|
||||
await db.delete(notes).where(eq(notes.id, params.id));
|
||||
db.delete(notes).where(eq(notes.id, params.id)).run();
|
||||
return json({ success: true });
|
||||
};
|
||||
|
||||
async function syncNoteTags(noteId: string, tagNames: string[]) {
|
||||
await db.delete(noteTags).where(eq(noteTags.noteId, noteId));
|
||||
|
||||
for (const name of tagNames) {
|
||||
let tagRow = await db.select().from(tags).where(eq(tags.name, name)).get();
|
||||
if (!tagRow) {
|
||||
const result = await db.insert(tags).values({ name }).returning();
|
||||
tagRow = result[0];
|
||||
}
|
||||
await db.insert(noteTags).values({ noteId, tagId: tagRow.id });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user