From 28e40d763da4b1c63984879722fe5ca536261f30 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Fri, 17 Apr 2026 17:08:22 +0200 Subject: [PATCH] feat(api): wishlist endpoints (list, add, remove, like, unlike) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/wishlist?sort=popular|newest|oldest&profile_id=… POST /api/wishlist { recipe_id, profile_id? } DELETE /api/wishlist/[recipe_id] PUT /api/wishlist/[recipe_id]/like { profile_id } DELETE /api/wishlist/[recipe_id]/like { profile_id } Co-Authored-By: Claude Opus 4.7 (1M context) --- src/routes/api/wishlist/+server.ts | 40 +++++++++++++++++++ .../api/wishlist/[recipe_id]/+server.ts | 16 ++++++++ .../api/wishlist/[recipe_id]/like/+server.ts | 31 ++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/routes/api/wishlist/+server.ts create mode 100644 src/routes/api/wishlist/[recipe_id]/+server.ts create mode 100644 src/routes/api/wishlist/[recipe_id]/like/+server.ts diff --git a/src/routes/api/wishlist/+server.ts b/src/routes/api/wishlist/+server.ts new file mode 100644 index 0000000..c6f39ad --- /dev/null +++ b/src/routes/api/wishlist/+server.ts @@ -0,0 +1,40 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { + addToWishlist, + listWishlist, + type SortKey +} from '$lib/server/wishlist/repository'; + +const AddSchema = z.object({ + recipe_id: z.number().int().positive(), + profile_id: z.number().int().positive().nullable().optional() +}); + +const VALID_SORTS: readonly SortKey[] = ['popular', 'newest', 'oldest'] as const; + +function parseSort(raw: string | null): SortKey { + return VALID_SORTS.includes(raw as SortKey) ? (raw as SortKey) : 'popular'; +} + +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 }) => { + const sort = parseSort(url.searchParams.get('sort')); + const profileId = parseProfileId(url.searchParams.get('profile_id')); + return json({ sort, entries: listWishlist(getDb(), profileId, sort) }); +}; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json().catch(() => null); + const parsed = AddSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + addToWishlist(getDb(), parsed.data.recipe_id, parsed.data.profile_id ?? null); + return json({ ok: true }, { status: 201 }); +}; diff --git a/src/routes/api/wishlist/[recipe_id]/+server.ts b/src/routes/api/wishlist/[recipe_id]/+server.ts new file mode 100644 index 0000000..41eb5da --- /dev/null +++ b/src/routes/api/wishlist/[recipe_id]/+server.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { getDb } from '$lib/server/db'; +import { removeFromWishlist } from '$lib/server/wishlist/repository'; + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid recipe_id' }); + return id; +} + +export const DELETE: RequestHandler = async ({ params }) => { + const id = parseId(params.recipe_id!); + removeFromWishlist(getDb(), id); + return json({ ok: true }); +}; diff --git a/src/routes/api/wishlist/[recipe_id]/like/+server.ts b/src/routes/api/wishlist/[recipe_id]/like/+server.ts new file mode 100644 index 0000000..2b7048e --- /dev/null +++ b/src/routes/api/wishlist/[recipe_id]/like/+server.ts @@ -0,0 +1,31 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { likeWish, unlikeWish } from '$lib/server/wishlist/repository'; + +const Schema = z.object({ profile_id: z.number().int().positive() }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid recipe_id' }); + return id; +} + +export const PUT: RequestHandler = async ({ params, request }) => { + const id = parseId(params.recipe_id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + likeWish(getDb(), id, parsed.data.profile_id); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async ({ params, request }) => { + const id = parseId(params.recipe_id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + unlikeWish(getDb(), id, parsed.data.profile_id); + return json({ ok: true }); +};