feat(api): /api/recipes/all akzeptiert sort=viewed + profile_id

VALID_SORTS um 'viewed' erweitert. parseProfileId-Helper analog zu
/api/wishlist. Wert wird an listAllRecipesPaginated als 5. Param
durchgereicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 14:31:17 +02:00
parent f495c024c6
commit b31223add5
2 changed files with 46 additions and 2 deletions

View File

@@ -6,7 +6,19 @@ import {
type AllRecipesSort type AllRecipesSort
} from '$lib/server/recipes/search-local'; } from '$lib/server/recipes/search-local';
const VALID_SORTS = new Set<AllRecipesSort>(['name', 'rating', 'cooked', 'created']); const VALID_SORTS = new Set<AllRecipesSort>([
'name',
'rating',
'cooked',
'created',
'viewed'
]);
function parseProfileId(raw: string | null): number | null {
if (!raw) return null;
const n = Number(raw);
return Number.isInteger(n) && n > 0 ? n : null;
}
export const GET: RequestHandler = async ({ url }) => { export const GET: RequestHandler = async ({ url }) => {
const sortRaw = (url.searchParams.get('sort') ?? 'name') as AllRecipesSort; const sortRaw = (url.searchParams.get('sort') ?? 'name') as AllRecipesSort;
@@ -17,6 +29,7 @@ export const GET: RequestHandler = async ({ url }) => {
// one round-trip so document height matches and scroll-restore lands. // one round-trip so document height matches and scroll-restore lands.
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get('limit') ?? 10))); const limit = Math.min(200, Math.max(1, Number(url.searchParams.get('limit') ?? 10)));
const offset = Math.max(0, Number(url.searchParams.get('offset') ?? 0)); const offset = Math.max(0, Number(url.searchParams.get('offset') ?? 0));
const hits = listAllRecipesPaginated(getDb(), sortRaw, limit, offset); const profileId = parseProfileId(url.searchParams.get('profile_id'));
const hits = listAllRecipesPaginated(getDb(), sortRaw, limit, offset, profileId);
return json({ sort: sortRaw, limit, offset, hits }); return json({ sort: sortRaw, limit, offset, hits });
}; };

View File

@@ -237,3 +237,34 @@ describe('POST /api/recipes/[id]/view', () => {
).rejects.toMatchObject({ status: 404 }); ).rejects.toMatchObject({ status: 404 });
}); });
}); });
// ---------------------------------------------------------------------------
// GET /api/recipes/all — sort=viewed + profile_id
// ---------------------------------------------------------------------------
import { GET as allGet } from '../../src/routes/api/recipes/all/+server';
describe('GET /api/recipes/all sort=viewed', () => {
it('passes profile_id through and returns viewed-order hits', async () => {
const db = openInMemoryForTest();
testDb.current = db;
const profile = createProfile(db, 'Test');
const a = seedRecipe(db, 'Apfel');
const b = seedRecipe(db, 'Birne');
recordView(db, profile.id, b);
await new Promise((r) => setTimeout(r, 1100));
recordView(db, profile.id, a);
const url = new URL(`http://localhost/api/recipes/all?sort=viewed&profile_id=${profile.id}&limit=10`);
const res = await allGet({ url } as never);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.sort).toBe('viewed');
expect(body.hits.map((h: { id: number }) => h.id)).toEqual([a, b]);
});
it('400 on invalid sort', async () => {
const url = new URL('http://localhost/api/recipes/all?sort=invalid');
await expect(allGet({ url } as never)).rejects.toMatchObject({ status: 400 });
});
});