feat(api): expose preview/import/profile/domain endpoints

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 15:12:59 +02:00
parent 5693371673
commit 86ff4c141a
8 changed files with 161 additions and 1 deletions

8
src/app.d.ts vendored
View File

@@ -1,4 +1,10 @@
declare global {
namespace App {}
namespace App {
interface Error {
message: string;
code?: string;
issues?: unknown;
}
}
}
export {};

17
src/lib/server/errors.ts Normal file
View File

@@ -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;
}

View File

@@ -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 });
}
};

View File

@@ -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 });
};

View File

@@ -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 });
}
};

View File

@@ -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 });
};

View File

@@ -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);
}
};

View File

@@ -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);
}
};