refactor: add mandatory db parameter to all service functions for testability

Make db a required first parameter on all service functions (notes-service,
api-utils, tags, collaborators, attachments, rate-limit, sync/server).
This enables unit testing with in-memory databases via createTestDb().

Also removes the NODE_ENV=test bypass from rate-limit and adds optional
now parameter for time control in tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-10 10:14:09 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 79f1e383c3
commit 8795a69b48
18 changed files with 135 additions and 134 deletions
+4 -3
View File
@@ -2,6 +2,7 @@ import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { verifyPassword, createSession } from '$lib/server/auth.js';
import { checkRateLimit, recordLoginAttempt } from '$lib/server/rate-limit.js';
import { db } from '$lib/server/db/index.js';
export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
const ip = getClientAddress();
@@ -11,7 +12,7 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress
throw error(400, 'Email and password are required');
}
const rateCheck = checkRateLimit(ip, email);
const rateCheck = checkRateLimit(db, ip, email);
if (!rateCheck.allowed) {
return json(
{ error: 'Too many login attempts. Please try again later.', retryAfter: rateCheck.retryAfter },
@@ -21,11 +22,11 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress
const user = await verifyPassword(email, password);
if (!user) {
recordLoginAttempt(ip, email, false);
recordLoginAttempt(db, ip, email, false);
throw error(401, 'Invalid email or password');
}
recordLoginAttempt(ip, email, true);
recordLoginAttempt(db, ip, email, true);
const userAgent = request.headers.get('user-agent') || undefined;
const token = await createSession(user.id, { userAgent, ip });
+3 -2
View File
@@ -1,5 +1,6 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { db } from '$lib/server/db/index.js';
import { getUserId } from '$lib/server/api-utils.js';
import { listNotes, createNote } from '$lib/server/notes-service.js';
import type { NoteFilter } from '$lib/types/index.js';
@@ -7,13 +8,13 @@ import type { NoteFilter } from '$lib/types/index.js';
export const GET: RequestHandler = async ({ url, ...event }) => {
const userId = getUserId(event);
const filter = (url.searchParams.get('filter') || 'all') as NoteFilter;
const result = listNotes(userId, filter);
const result = listNotes(db, userId, filter);
return json(result);
};
export const POST: RequestHandler = async ({ request, ...event }) => {
const userId = getUserId(event);
const body = await request.json();
const newNote = createNote(userId, body);
const newNote = createNote(db, userId, body);
return json(newNote, { status: 201 });
};
+5 -4
View File
@@ -1,11 +1,12 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { db } from '$lib/server/db/index.js';
import { getUserId, requireNoteAccess, requireNoteOwnership } from '$lib/server/api-utils.js';
import { getNote, updateNote, deleteNote } from '$lib/server/notes-service.js';
export const GET: RequestHandler = async ({ params, ...event }) => {
const userId = getUserId(event);
const note = getNote(userId, params.id);
const note = getNote(db, userId, params.id);
if (!note) throw error(404, 'Note not found');
return json(note);
@@ -14,7 +15,7 @@ export const GET: RequestHandler = async ({ params, ...event }) => {
export const PATCH: RequestHandler = async ({ params, request, ...event }) => {
const userId = getUserId(event);
const body = await request.json();
const updated = updateNote(userId, params.id, body);
const updated = updateNote(db, userId, params.id, body);
if (!updated) throw error(404, 'Note not found');
return json(updated);
@@ -22,9 +23,9 @@ export const PATCH: RequestHandler = async ({ params, request, ...event }) => {
export const DELETE: RequestHandler = async ({ params, ...event }) => {
const userId = getUserId(event);
requireNoteOwnership(params.id, userId);
requireNoteOwnership(db, params.id, userId);
const deleted = deleteNote(userId, params.id);
const deleted = deleteNote(db, userId, params.id);
if (!deleted) throw error(404, 'Note not found');
return json({ success: true });
@@ -2,16 +2,17 @@ import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { saveAttachment, getAttachmentsByNote, getAttachment, deleteAttachment, updateAttachment } from '$lib/server/attachments.js';
import { readFile } from 'fs/promises';
import { db } from '$lib/server/db/index.js';
import { getUserId, requireNoteAccess } from '$lib/server/api-utils.js';
export const GET: RequestHandler = async ({ params, url, ...event }) => {
const userId = getUserId(event);
requireNoteAccess(params.id, userId);
requireNoteAccess(db, params.id, userId);
const attachmentId = url.searchParams.get('attachmentId');
if (attachmentId) {
const attachment = await getAttachment(attachmentId, params.id);
const attachment = await getAttachment(db, attachmentId, params.id);
if (!attachment) throw error(404, 'Attachment not found');
const wantThumb = url.searchParams.get('thumb') === '1';
@@ -28,13 +29,13 @@ export const GET: RequestHandler = async ({ params, url, ...event }) => {
});
}
const list = await getAttachmentsByNote(params.id);
const list = await getAttachmentsByNote(db, params.id);
return json(list);
};
export const POST: RequestHandler = async ({ params, request, ...event }) => {
const userId = getUserId(event);
requireNoteAccess(params.id, userId);
requireNoteAccess(db, params.id, userId);
const formData = await request.formData();
const file = formData.get('file') as File;
@@ -53,13 +54,13 @@ export const POST: RequestHandler = async ({ params, request, ...event }) => {
}
const thumbnail = formData.get('thumbnail') as File | null;
const attachment = await saveAttachment(params.id, file, userId, thumbnail);
const attachment = await saveAttachment(db, params.id, file, userId, thumbnail);
return json(attachment, { status: 201 });
};
export const PATCH: RequestHandler = async ({ params, url, request, ...event }) => {
const userId = getUserId(event);
requireNoteAccess(params.id, userId);
requireNoteAccess(db, params.id, userId);
const attachmentId = url.searchParams.get('attachmentId');
if (!attachmentId) throw error(400, 'attachmentId required');
@@ -67,7 +68,7 @@ export const PATCH: RequestHandler = async ({ params, url, request, ...event })
const body = await request.json();
if (typeof body.featured !== 'boolean') throw error(400, 'featured (boolean) required');
const updated = await updateAttachment(attachmentId, params.id, { featured: body.featured });
const updated = await updateAttachment(db, attachmentId, params.id, { featured: body.featured });
if (!updated) throw error(404, 'Attachment not found');
return json(updated);
@@ -75,11 +76,11 @@ export const PATCH: RequestHandler = async ({ params, url, request, ...event })
export const DELETE: RequestHandler = async ({ params, url, ...event }) => {
const userId = getUserId(event);
requireNoteAccess(params.id, userId);
requireNoteAccess(db, params.id, userId);
const attachmentId = url.searchParams.get('attachmentId');
if (!attachmentId) throw error(400, 'attachmentId required');
await deleteAttachment(attachmentId, params.id);
await deleteAttachment(db, attachmentId, params.id);
return json({ success: true });
};
@@ -8,15 +8,15 @@ import { eq, and } from 'drizzle-orm';
export const GET: RequestHandler = async ({ params, ...event }) => {
const userId = getUserId(event);
requireNoteAccess(params.id, userId);
requireNoteAccess(db, params.id, userId);
const collaborators = fetchCollaboratorsForNotes([params.id]);
const collaborators = fetchCollaboratorsForNotes(db, [params.id]);
return json(collaborators.get(params.id) ?? []);
};
export const POST: RequestHandler = async ({ params, request, ...event }) => {
const userId = getUserId(event);
requireNoteOwnership(params.id, userId);
requireNoteOwnership(db, params.id, userId);
const body = await request.json();
const targetUserId = body.userId;
@@ -46,9 +46,9 @@ export const POST: RequestHandler = async ({ params, request, ...event }) => {
throw error(409, 'User is already a collaborator');
}
addCollaborator(params.id, targetUserId, userId);
addCollaborator(db, params.id, targetUserId, userId);
const collaborators = fetchCollaboratorsForNotes([params.id]);
const collaborators = fetchCollaboratorsForNotes(db, [params.id]);
return json(collaborators.get(params.id) ?? [], { status: 201 });
};
@@ -60,14 +60,14 @@ export const DELETE: RequestHandler = async ({ params, url, ...event }) => {
throw error(400, 'userId query parameter is required');
}
const { isOwner } = requireNoteAccess(params.id, userId);
const { isOwner } = requireNoteAccess(db, params.id, userId);
// Owner can remove anyone. Collaborator can only remove self.
if (!isOwner && targetUserId !== userId) {
throw error(403, 'Only the owner can remove other collaborators');
}
removeCollaborator(params.id, targetUserId);
removeCollaborator(db, params.id, targetUserId);
return json({ success: true });
};
+2 -1
View File
@@ -1,5 +1,6 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { db } from '$lib/server/db/index.js';
import { reorderNotes } from '$lib/server/notes-service.js';
import { getUserId } from '$lib/server/api-utils.js';
@@ -12,6 +13,6 @@ export const POST: RequestHandler = async ({ request, ...event }) => {
return json({ error: 'Invalid payload' }, { status: 400 });
}
reorderNotes(userId, orders);
reorderNotes(db, userId, orders);
return json({ ok: true });
};
+2 -1
View File
@@ -1,5 +1,6 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { db } from '$lib/server/db/index.js';
import { getUserId } from '$lib/server/api-utils.js';
import { searchNotes } from '$lib/server/notes-service.js';
@@ -11,6 +12,6 @@ export const GET: RequestHandler = async ({ url, ...event }) => {
return json([]);
}
const results = searchNotes(userId, query);
const results = searchNotes(db, userId, query);
return json(results);
};
+5 -4
View File
@@ -4,6 +4,7 @@ import { processSyncPush, getChangesSince } from '$lib/sync/server.js';
import { fetchTagsForNotes } from '$lib/server/tags.js';
import { fetchAttachmentsForNotes } from '$lib/server/attachments.js';
import { getUserId } from '$lib/server/api-utils.js';
import { db } from '$lib/server/db/index.js';
export const POST: RequestHandler = async ({ request, ...event }) => {
const userId = getUserId(event);
@@ -13,18 +14,18 @@ export const POST: RequestHandler = async ({ request, ...event }) => {
return json({ error: 'Invalid changes' }, { status: 400 });
}
await processSyncPush(changes, userId);
await processSyncPush(db, changes, userId);
return json({ success: true });
};
export const GET: RequestHandler = async ({ url, ...event }) => {
const userId = getUserId(event);
const since = parseInt(url.searchParams.get('since') || '0', 10);
const changes = await getChangesSince(since, userId);
const changes = await getChangesSince(db, since, userId);
const noteIds = changes.map((n) => n.id);
const tagMap = fetchTagsForNotes(noteIds);
const attachmentMap = fetchAttachmentsForNotes(noteIds);
const tagMap = fetchTagsForNotes(db, noteIds);
const attachmentMap = fetchAttachmentsForNotes(db, noteIds);
const changesWithTags = changes.map((note) => ({
...note,
tags: tagMap.get(note.id) ?? [],