diff --git a/src/app.d.ts b/src/app.d.ts index b1e1051..f76e45f 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,10 @@ declare global { - namespace App {} + namespace App { + interface Error { + message: string; + code?: string; + issues?: unknown; + } + } } export {}; diff --git a/src/lib/server/errors.ts b/src/lib/server/errors.ts new file mode 100644 index 0000000..2cfecea --- /dev/null +++ b/src/lib/server/errors.ts @@ -0,0 +1,17 @@ +import { error } from '@sveltejs/kit'; +import { ImporterError } from './recipes/importer'; + +export function mapImporterError(e: unknown): never { + if (e instanceof ImporterError) { + const status = + e.code === 'INVALID_URL' || e.code === 'DOMAIN_BLOCKED' + ? e.code === 'DOMAIN_BLOCKED' + ? 403 + : 400 + : e.code === 'NO_RECIPE_FOUND' + ? 422 + : 502; // FETCH_FAILED + error(status, { message: e.message, code: e.code }); + } + throw e; +} diff --git a/src/routes/api/domains/+server.ts b/src/routes/api/domains/+server.ts new file mode 100644 index 0000000..766e037 --- /dev/null +++ b/src/routes/api/domains/+server.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { addDomain, listDomains } from '$lib/server/domains/repository'; + +const CreateSchema = z.object({ + domain: z.string().min(3).max(253), + display_name: z.string().max(100).nullable().optional(), + added_by_profile_id: z.number().int().positive().nullable().optional() +}); + +export const GET: RequestHandler = async () => { + return json(listDomains(getDb())); +}; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json().catch(() => null); + const parsed = CreateSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + try { + const d = addDomain( + getDb(), + parsed.data.domain, + parsed.data.display_name ?? null, + parsed.data.added_by_profile_id ?? null + ); + return json(d, { status: 201 }); + } catch (e) { + error(409, { message: (e as Error).message }); + } +}; diff --git a/src/routes/api/domains/[id]/+server.ts b/src/routes/api/domains/[id]/+server.ts new file mode 100644 index 0000000..85b23c1 --- /dev/null +++ b/src/routes/api/domains/[id]/+server.ts @@ -0,0 +1,11 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { getDb } from '$lib/server/db'; +import { removeDomain } from '$lib/server/domains/repository'; + +export const DELETE: RequestHandler = async ({ params }) => { + const id = Number(params.id); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + removeDomain(getDb(), id); + return json({ ok: true }); +}; diff --git a/src/routes/api/profiles/+server.ts b/src/routes/api/profiles/+server.ts new file mode 100644 index 0000000..8ec51da --- /dev/null +++ b/src/routes/api/profiles/+server.ts @@ -0,0 +1,28 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { createProfile, listProfiles } from '$lib/server/profiles/repository'; + +const CreateSchema = z.object({ + name: z.string().min(1).max(50), + avatar_emoji: z.string().max(16).nullable().optional() +}); + +export const GET: RequestHandler = async () => { + return json(listProfiles(getDb())); +}; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json().catch(() => null); + const parsed = CreateSchema.safeParse(body); + if (!parsed.success) { + error(400, { message: 'Invalid body', issues: parsed.error.issues }); + } + try { + const p = createProfile(getDb(), parsed.data.name, parsed.data.avatar_emoji ?? null); + return json(p, { status: 201 }); + } catch (e) { + error(409, { message: (e as Error).message }); + } +}; diff --git a/src/routes/api/profiles/[id]/+server.ts b/src/routes/api/profiles/[id]/+server.ts new file mode 100644 index 0000000..289ef26 --- /dev/null +++ b/src/routes/api/profiles/[id]/+server.ts @@ -0,0 +1,28 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { deleteProfile, renameProfile } from '$lib/server/profiles/repository'; + +const RenameSchema = z.object({ name: z.string().min(1).max(50) }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + return id; +} + +export const PATCH: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = RenameSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + renameProfile(getDb(), id, parsed.data.name); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async ({ params }) => { + const id = parseId(params.id!); + deleteProfile(getDb(), id); + return json({ ok: true }); +}; diff --git a/src/routes/api/recipes/import/+server.ts b/src/routes/api/recipes/import/+server.ts new file mode 100644 index 0000000..429e6fd --- /dev/null +++ b/src/routes/api/recipes/import/+server.ts @@ -0,0 +1,22 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { importRecipe } from '$lib/server/recipes/importer'; +import { mapImporterError } from '$lib/server/errors'; + +const ImportSchema = z.object({ url: z.string().url() }); + +const IMAGE_DIR = process.env.IMAGE_DIR ?? './data/images'; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json().catch(() => null); + const parsed = ImportSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + try { + const result = await importRecipe(getDb(), IMAGE_DIR, parsed.data.url); + return json({ id: result.id, duplicate: result.duplicate }); + } catch (e) { + mapImporterError(e); + } +}; diff --git a/src/routes/api/recipes/preview/+server.ts b/src/routes/api/recipes/preview/+server.ts new file mode 100644 index 0000000..11d903d --- /dev/null +++ b/src/routes/api/recipes/preview/+server.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { getDb } from '$lib/server/db'; +import { previewRecipe } from '$lib/server/recipes/importer'; +import { mapImporterError } from '$lib/server/errors'; + +export const GET: RequestHandler = async ({ url }) => { + const target = url.searchParams.get('url'); + if (!target) error(400, { message: 'Missing ?url=' }); + try { + const recipe = await previewRecipe(getDb(), target); + return json(recipe); + } catch (e) { + mapImporterError(e); + } +};